From 91e194556c52d2f354344f930419eef2dd6267f0 Mon Sep 17 00:00:00 2001
From: Pierre Schmitz $longLine Test\nContent of Template:Foo\nContent of Template:Bar\n
+ * protected function setUp() {
+ * $this->setMwGlobals( 'wgRestrictStuff', true );
+ * }
+ *
+ * function testFoo() {}
+ *
+ * function testBar() {}
+ * $this->assertTrue( self::getX()->doStuff() );
+ *
+ * $this->setMwGlobals( 'wgRestrictStuff', false );
+ * $this->assertTrue( self::getX()->doStuff() );
+ * }
+ *
+ * function testQuux() {}
+ *
+ *
+ * @param array|string $pairs Key to the global variable, or an array
+ * of key/value pairs.
+ * @param mixed $value Value to set the global to (ignored
+ * if an array is given as first argument).
+ */
+ protected function setMwGlobals( $pairs, $value = null ) {
+
+ // Normalize (string, value) to an array
+ if ( is_string( $pairs ) ) {
+ $pairs = array( $pairs => $value );
+ }
+
+ foreach ( $pairs as $key => $value ) {
+ // NOTE: make sure we only save the global once or a second call to
+ // setMwGlobals() on the same global would override the original
+ // value.
+ if ( !array_key_exists( $key, $this->mwGlobals ) ) {
+ $this->mwGlobals[$key] = $GLOBALS[$key];
+ }
+
+ // Override the global
+ $GLOBALS[$key] = $value;
+ }
+ }
+
+ /**
+ * Merges the given values into a MW global array variable.
+ * Useful for setting some entries in a configuration array, instead of
+ * setting the entire array.
+ *
+ * @param String $name The name of the global, as in wgFooBar
+ * @param Array $values The array containing the entries to set in that global
+ *
+ * @throws MWException if the designated global is not an array.
+ */
+ protected function mergeMwGlobalArrayValue( $name, $values ) {
+ if ( !isset( $GLOBALS[$name] ) ) {
+ $merged = $values;
+ } else {
+ if ( !is_array( $GLOBALS[$name] ) ) {
+ throw new MWException( "MW global $name is not an array." );
+ }
+
+ // NOTE: do not use array_merge, it screws up for numeric keys.
+ $merged = $GLOBALS[$name];
+ foreach ( $values as $k => $v ) {
+ $merged[$k] = $v;
+ }
+ }
+
+ $this->setMwGlobals( $name, $merged );
+ }
+
+ function dbPrefix() {
+ return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
+ }
+
+ function needsDB() {
+ # if the test says it uses database tables, it needs the database
+ if ( $this->tablesUsed ) {
+ return true;
+ }
+
+ # if the test says it belongs to the Database group, it needs the database
+ $rc = new ReflectionClass( $this );
+ if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
+ return true;
+ }
+
+ return 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() {
+ # disabled for performance
+ #$this->tablesUsed[] = 'page';
+ #$this->tablesUsed[] = 'revision';
+
+ if ( $this->db->getType() == 'oracle' ) {
+
+ # Insert 0 user to prevent FK violations
+ # Anonymous user
+ $this->db->insert( 'user', array(
+ 'user_id' => 0,
+ 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) );
+
+ # Insert 0 page to prevent FK violations
+ # Blank page
+ $this->db->insert( 'page', array(
+ 'page_id' => 0,
+ 'page_namespace' => 0,
+ 'page_title' => ' ',
+ 'page_restrictions' => null,
+ 'page_counter' => 0,
+ 'page_is_redirect' => 0,
+ 'page_is_new' => 0,
+ 'page_random' => 0,
+ 'page_touched' => $this->db->timestamp(),
+ 'page_latest' => 0,
+ 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) );
+
+ }
+
+ 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
+ $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ if ( !$page->getId() == 0 ) {
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW,
+ false,
+ User::newFromName( 'UTSysop' ) );
+ }
+ }
+
+ /**
+ * Restores MediaWiki to using the table set (table prefix) it was using before
+ * setupTestDB() was called. Useful if we need to perform database operations
+ * after the test run has finished (such as saving logs or profiling info).
+ */
+ public static function teardownTestDB() {
+ if ( !self::$dbSetup ) {
+ return;
+ }
+
+ CloneDatabase::changePrefix( self::$oldTablePrefix );
+
+ self::$oldTablePrefix = false;
+ self::$dbSetup = false;
+ }
+
+ /**
+ * Creates an empty skeleton of the wiki database by cloning its structure
+ * to equivalent tables using the given $prefix. Then sets MediaWiki to
+ * use the new set of tables (aka schema) instead of the original set.
+ *
+ * This is used to generate a dummy table set, typically consisting of temporary
+ * tables, that will be used by tests instead of the original wiki database tables.
+ *
+ * @note: the original table prefix is stored in self::$oldTablePrefix. This is used
+ * by teardownTestDB() to return the wiki to using the original table set.
+ *
+ * @note: this method only works when first called. Subsequent calls have no effect,
+ * even if using different parameters.
+ *
+ * @param DatabaseBase $db The database connection
+ * @param String $prefix The prefix to use for the new table set (aka schema).
+ *
+ * @throws MWException if the database table prefix is already $prefix
+ */
+ public static function setupTestDB( DatabaseBase $db, $prefix ) {
+ global $wgDBprefix;
+ if ( $wgDBprefix === $prefix ) {
+ throw new MWException( 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
+ }
+
+ if ( self::$dbSetup ) {
+ return;
+ }
+
+ $tablesCloned = self::listTables( $db );
+ $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
+ $dbClone->useTemporaryTables( self::$useTemporaryTables );
+
+ self::$dbSetup = true;
+ self::$oldTablePrefix = $wgDBprefix;
+
+ if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
+ CloneDatabase::changePrefix( $prefix );
+ return;
+ } else {
+ $dbClone->cloneTableStructure();
+ }
+
+ if ( $db->getType() == 'oracle' ) {
+ $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ }
+ }
+
+ /**
+ * Empty all tables so they can be repopulated for tests
+ */
+ private function resetDB() {
+ if ( $this->db ) {
+ if ( $this->db->getType() == 'oracle' ) {
+ if ( self::$useTemporaryTables ) {
+ wfGetLB()->closeAll();
+ $this->db = wfGetDB( DB_MASTER );
+ } else {
+ foreach ( $this->tablesUsed as $tbl ) {
+ if ( $tbl == 'interwiki' ) {
+ continue;
+ }
+ $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
+ }
+ }
+ } else {
+ foreach ( $this->tablesUsed as $tbl ) {
+ if ( $tbl == 'interwiki' || $tbl == 'user' ) {
+ continue;
+ }
+ $this->db->delete( $tbl, '*', __METHOD__ );
+ }
+ }
+ }
+ }
+
+ 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 );
+ }
+
+ private static function unprefixTable( $tableName ) {
+ global $wgDBprefix;
+ return substr( $tableName, strlen( $wgDBprefix ) );
+ }
+
+ private static function isNotUnittest( $table ) {
+ return strpos( $table, 'unittest_' ) !== 0;
+ }
+
+ public static function listTables( $db ) {
+ global $wgDBprefix;
+
+ $tables = $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 ( $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;
+
+ }
+
+ /**
+ * Don't throw a warning if $function is deprecated and called later
+ *
+ * @param $function String
+ * @return null
+ */
+ function hideDeprecated( $function ) {
+ wfSuppressWarnings();
+ wfDeprecated( $function );
+ wfRestoreWarnings();
+ }
+
+ /**
+ * Asserts that the given database query yields the rows given by $expectedRows.
+ * The expected rows should be given as indexed (not associative) arrays, with
+ * the values given in the order of the columns in the $fields parameter.
+ * Note that the rows are sorted by the columns given in $fields.
+ *
+ * @since 1.20
+ *
+ * @param $table String|Array the table(s) to query
+ * @param $fields String|Array the columns to include in the result (and to sort by)
+ * @param $condition String|Array "where" condition(s)
+ * @param $expectedRows Array - an array of arrays giving the expected rows.
+ *
+ * @throws MWException if this test cases's needsDB() method doesn't return true.
+ * Test cases can use "@group Database" to enable database test support,
+ * or list the tables under testing in $this->tablesUsed, or override the
+ * needsDB() method.
+ */
+ protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
+ if ( !$this->needsDB() ) {
+ throw new MWException( 'When testing database state, the test cases\'s needDB()' .
+ ' method should return true. Use @group Database or $this->tablesUsed.' );
+ }
+
+ $db = wfGetDB( DB_SLAVE );
+
+ $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) );
+ $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
+
+ $i = 0;
+
+ foreach ( $expectedRows as $expected ) {
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $i += 1;
+ $this->assertNotEmpty( $r, "row #$i missing" );
+
+ $this->assertEquals( $expected, $r, "row #$i mismatches" );
+ }
+
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $this->assertFalse( $r, "found extra row (after #$i)" );
+ }
+
+ /**
+ * Utility method taking an array of elements and wrapping
+ * each element in it's own array. Useful for data providers
+ * that only return a single argument.
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @return array
+ */
+ protected function arrayWrap( array $elements ) {
+ return array_map(
+ function ( $element ) {
+ return array( $element );
+ },
+ $elements
+ );
+ }
+
+ /**
+ * Assert that two arrays are equal. By default this means that both arrays need to hold
+ * the same set of values. Using additional arguments, order and associated key can also
+ * be set as relevant.
+ *
+ * @since 1.20
+ *
+ * @param array $expected
+ * @param array $actual
+ * @param boolean $ordered If the order of the values should match
+ * @param boolean $named If the keys should match
+ */
+ protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) {
+ if ( !$ordered ) {
+ $this->objectAssociativeSort( $expected );
+ $this->objectAssociativeSort( $actual );
+ }
+
+ if ( !$named ) {
+ $expected = array_values( $expected );
+ $actual = array_values( $actual );
+ }
+
+ call_user_func_array(
+ array( $this, 'assertEquals' ),
+ array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) )
+ );
+ }
+
+ /**
+ * Put each HTML element on its own line and then equals() the results
+ *
+ * Use for nicely formatting of PHPUnit diff output when comparing very
+ * simple HTML
+ *
+ * @since 1.20
+ *
+ * @param String $expected HTML on oneline
+ * @param String $actual HTML on oneline
+ * @param String $msg Optional message
+ */
+ protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
+ $expected = str_replace( '>', ">\n", $expected );
+ $actual = str_replace( '>', ">\n", $actual );
+
+ $this->assertEquals( $expected, $actual, $msg );
+ }
+
+ /**
+ * Does an associative sort that works for objects.
+ *
+ * @since 1.20
+ *
+ * @param array $array
+ */
+ protected function objectAssociativeSort( array &$array ) {
+ uasort(
+ $array,
+ function ( $a, $b ) {
+ return serialize( $a ) > serialize( $b ) ? 1 : -1;
+ }
+ );
+ }
+
+ /**
+ * Utility function for eliminating all string keys from an array.
+ * Useful to turn a database result row as returned by fetchRow() into
+ * a pure indexed array.
+ *
+ * @since 1.20
+ *
+ * @param $r mixed the array to remove string keys from.
+ */
+ protected static function stripStringKeys( &$r ) {
+ if ( !is_array( $r ) ) {
+ return;
+ }
+
+ foreach ( $r as $k => $v ) {
+ if ( is_string( $k ) ) {
+ unset( $r[$k] );
+ }
+ }
+ }
+
+ /**
+ * Asserts that the provided variable is of the specified
+ * internal type or equals the $value argument. This is useful
+ * for testing return types of functions that return a certain
+ * type or *value* when not set or on error.
+ *
+ * @since 1.20
+ *
+ * @param string $type
+ * @param mixed $actual
+ * @param mixed $value
+ * @param string $message
+ */
+ protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
+ if ( $actual === $value ) {
+ $this->assertTrue( true, $message );
+ } else {
+ $this->assertType( $type, $actual, $message );
+ }
+ }
+
+ /**
+ * Asserts the type of the provided value. This can be either
+ * in internal type such as boolean or integer, or a class or
+ * interface the value extends or implements.
+ *
+ * @since 1.20
+ *
+ * @param string $type
+ * @param mixed $actual
+ * @param string $message
+ */
+ protected function assertType( $type, $actual, $message = '' ) {
+ if ( class_exists( $type ) || interface_exists( $type ) ) {
+ $this->assertInstanceOf( $type, $actual, $message );
+ } else {
+ $this->assertInternalType( $type, $actual, $message );
+ }
+ }
+
+ /**
+ * Returns true iff the given namespace defaults to Wikitext
+ * according to $wgNamespaceContentModels
+ *
+ * @param int $ns The namespace ID to check
+ *
+ * @return bool
+ * @since 1.21
+ */
+ protected function isWikitextNS( $ns ) {
+ global $wgNamespaceContentModels;
+
+ if ( isset( $wgNamespaceContentModels[$ns] ) ) {
+ return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the ID of a namespace that defaults to Wikitext.
+ * Throws an MWException if there is none.
+ *
+ * @return int the ID of the wikitext Namespace
+ * @since 1.21
+ */
+ protected function getDefaultWikitextNS() {
+ global $wgNamespaceContentModels;
+
+ static $wikitextNS = null; // this is not going to change
+ if ( $wikitextNS !== null ) {
+ return $wikitextNS;
+ }
+
+ // quickly short out on most common case:
+ if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
+ return NS_MAIN;
+ }
+
+ // NOTE: prefer content namespaces
+ $namespaces = array_unique( array_merge(
+ MWNamespace::getContentNamespaces(),
+ array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these
+ MWNamespace::getValidNamespaces()
+ ) );
+
+ $namespaces = array_diff( $namespaces, array(
+ NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
+ ) );
+
+ $talk = array_filter( $namespaces, function ( $ns ) {
+ return MWNamespace::isTalk( $ns );
+ } );
+
+ // prefer non-talk pages
+ $namespaces = array_diff( $namespaces, $talk );
+ $namespaces = array_merge( $namespaces, $talk );
+
+ // check default content model of each namespace
+ foreach ( $namespaces as $ns ) {
+ if ( !isset( $wgNamespaceContentModels[$ns] ) ||
+ $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
+ ) {
+
+ $wikitextNS = $ns;
+ return $wikitextNS;
+ }
+ }
+
+ // give up
+ // @todo: Inside a test, we could skip the test as incomplete.
+ // But frequently, this is used in fixture setup.
+ throw new MWException( "No namespace defaults to wikitext!" );
+ }
+
+ /**
+ * Check, if $wgDiff3 is set and ready to merge
+ * Will mark the calling test as skipped, if not ready
+ *
+ * @since 1.21
+ */
+ protected function checkHasDiff3() {
+ global $wgDiff3;
+
+ # This check may also protect against code injection in
+ # case of broken installations.
+ wfSuppressWarnings();
+ $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
+ wfRestoreWarnings();
+
+ if ( !$haveDiff3 ) {
+ $this->markTestSkipped( "Skip test, since diff3 is not configured" );
+ }
+ }
+
+ /**
+ * Check whether we have the 'gzip' commandline utility, will skip
+ * the test whenever "gzip -V" fails.
+ *
+ * Result is cached at the process level.
+ *
+ * @return bool
+ *
+ * @since 1.21
+ */
+ protected function checkHasGzip() {
+ static $haveGzip;
+
+ if ( $haveGzip === null ) {
+ $retval = null;
+ wfShellExec( 'gzip -V', $retval );
+ $haveGzip = ( $retval === 0 );
+ }
+
+ if ( !$haveGzip ) {
+ $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
+ }
+
+ return $haveGzip;
+ }
+
+ /**
+ * Check if $extName is a loaded PHP extension, will skip the
+ * test whenever it is not loaded.
+ *
+ * @since 1.21
+ */
+ protected function checkPHPExtension( $extName ) {
+ $loaded = extension_loaded( $extName );
+ if ( !$loaded ) {
+ $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
+ }
+ return $loaded;
+ }
+
+ /**
+ * Asserts that an exception of the specified type occurs when running
+ * the provided code.
+ *
+ * @since 1.21
+ *
+ * @param callable $code
+ * @param string $expected
+ * @param string $message
+ */
+ protected function assertException( $code, $expected = 'Exception', $message = '' ) {
+ $pokemons = null;
+
+ try {
+ call_user_func( $code );
+ } catch ( Exception $pokemons ) {
+ // Gotta Catch 'Em All!
+ }
+
+ if ( $message === '' ) {
+ $message = 'An exception of type "' . $expected . '" should have been thrown';
+ }
+
+ $this->assertInstanceOf( $expected, $pokemons, $message );
+ }
+
+}
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/StructureTest.php b/tests/phpunit/StructureTest.php
new file mode 100644
index 00000000..a9420981
--- /dev/null
+++ b/tests/phpunit/StructureTest.php
@@ -0,0 +1,63 @@
+markTestSkipped( 'This test does not work on Windows' );
+ }
+ $rootPath = escapeshellarg( __DIR__ );
+ $testClassRegex = implode( '|', array(
+ 'ApiFormatTestBase',
+ 'ApiTestCase',
+ 'ApiQueryTestBase',
+ 'ApiQueryContinueTestBase',
+ 'MediaWikiLangTestCase',
+ 'MediaWikiTestCase',
+ 'PHPUnit_Framework_TestCase',
+ 'DumpTestCase',
+ ) );
+ $testClassRegex = "^class .* extends ($testClassRegex)";
+ $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" .
+ " | xargs grep -El '$testClassRegex|function suite\('";
+
+ $results = null;
+ $exitCode = null;
+ exec( $finder, $results, $exitCode );
+
+ $this->assertEquals(
+ 0,
+ $exitCode,
+ 'Verify find/grep command succeeds.'
+ );
+
+ $results = array_filter(
+ $results,
+ array( $this, 'filterSuites' )
+ );
+ $strip = strlen( $rootPath ) - 1;
+ foreach ( $results as $k => $v ) {
+ $results[$k] = substr( $v, $strip );
+ }
+ $this->assertEquals(
+ array(),
+ $results,
+ "Unit test file in $rootPath must end with Test."
+ );
+ }
+
+ /**
+ * Filter to remove testUnitTestFileNamesEndWithTest false positives.
+ */
+ public function filterSuites( $filename ) {
+ return strpos( $filename, __DIR__ . '/suites/' ) !== 0;
+ }
+}
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..01caf8f4
--- /dev/null
+++ b/tests/phpunit/bootstrap.php
@@ -0,0 +1,32 @@
+
+
+]>
+
+
\ 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
new file mode 100644
index 00000000..a0b81ca9
Binary files /dev/null and b/tests/phpunit/data/media/Png-native-test.png differ
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 @@
+
+
+
\nhello <world>\n\n" + ), + array( + 'MediaWiki:Test.css', + null, + "/* hello [[world]] */\n", + "
\n/* hello [[world]] */\n\n", + array( + 'Links' => array( + array( 'World' => 0 ) + ) + ) + ), + + // TODO: more...? + ); + } + + public function testGetModel() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() ); + } + + public function testGetContentHandler() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() ); + } + + public static function dataEquals() { + return array( + array( new CssContent( 'hallo' ), null, false ), + array( new CssContent( 'hallo' ), new CssContent( 'hallo' ), true ), + array( new CssContent( 'hallo' ), new WikitextContent( 'hallo' ), false ), + array( new CssContent( 'hallo' ), new CssContent( 'HALLO' ), false ), + ); + } + + /** + * @dataProvider dataEquals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } + +} diff --git a/tests/phpunit/includes/content/JavaScriptContentTest.php b/tests/phpunit/includes/content/JavaScriptContentTest.php new file mode 100644 index 00000000..2d693feb --- /dev/null +++ b/tests/phpunit/includes/content/JavaScriptContentTest.php @@ -0,0 +1,273 @@ +\n", + "
\nhello <world>\n\n" + ), + array( + 'MediaWiki:Test.js', + null, + "hello(); // [[world]]\n", + "
\nhello(); // [[world]]\n\n", + array( + 'Links' => array( + array( 'World' => 0 ) + ) + ) + ), + + // TODO: more...? + ); + } + + // XXX: Unused function + public static function dataGetSection() { + return array( + array( WikitextContentTest::$sections, + '0', + null + ), + array( WikitextContentTest::$sections, + '2', + null + ), + array( WikitextContentTest::$sections, + '8', + null + ), + ); + } + + // XXX: Unused function + public static function dataReplaceSection() { + return array( + array( WikitextContentTest::$sections, + '0', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + '', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + '2', + "== TEST ==\nmore fun", + null, + null + ), + array( WikitextContentTest::$sections, + '8', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + 'new', + 'No more', + 'New', + null + ), + ); + } + + public function testAddSectionHeader() { + $content = $this->newContent( 'hello world' ); + $c = $content->addSectionHeader( 'test' ); + + $this->assertTrue( $content->equals( $c ) ); + } + + // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional. + public static function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is
foo', + ), $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' ); + } + + // TODO: Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText() +} diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php new file mode 100644 index 00000000..68f77ab5 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserOutputTest.php @@ -0,0 +1,55 @@ +assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) ); + } + + public function testExtensionData() { + $po = new ParserOutput(); + + $po->setExtensionData( "one", "Foo" ); + + $this->assertEquals( "Foo", $po->getExtensionData( "one" ) ); + $this->assertNull( $po->getExtensionData( "spam" ) ); + + $po->setExtensionData( "two", "Bar" ); + $this->assertEquals( "Foo", $po->getExtensionData( "one" ) ); + $this->assertEquals( "Bar", $po->getExtensionData( "two" ) ); + + $po->setExtensionData( "one", null ); + $this->assertNull( $po->getExtensionData( "one" ) ); + $this->assertEquals( "Bar", $po->getExtensionData( "two" ) ); + } +} diff --git a/tests/phpunit/includes/parser/ParserPreloadTest.php b/tests/phpunit/includes/parser/ParserPreloadTest.php new file mode 100644 index 00000000..e16b407e --- /dev/null +++ b/tests/phpunit/includes/parser/ParserPreloadTest.php @@ -0,0 +1,72 @@ +testParserOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang ); + + $this->testParser = new Parser(); + $this->testParser->Options( $this->testParserOptions ); + $this->testParser->clearState(); + + $this->title = Title::newFromText( 'Preload Test' ); + } + + protected function tearDown() { + parent::tearDown(); + + unset( $this->testParser ); + unset( $this->title ); + } + + /** + * @covers Parser::getPreloadText + */ + function testPreloadSimpleText() { + $this->assertPreloaded( 'simple', 'simple' ); + } + + /** + * @covers Parser::getPreloadText + */ + function testPreloadedPreIsUnstripped() { + $this->assertPreloaded( + '
monospaced', + '
monospaced', + '
in preloaded text must be unstripped (bug 27467)' + ); + } + + /** + * @covers Parser::getPreloadText + */ + function testPreloadedNowikiIsUnstripped() { + $this->assertPreloaded( + '[[Dummy title]] ', + '[[Dummy title]] ', + 'in preloaded text must be unstripped (bug 27467)' + ); + } + + function assertPreloaded( $expected, $text, $msg = '' ) { + $this->assertEquals( + $expected, + $this->testParser->getPreloadText( + $text, + $this->title, + $this->testParserOptions + ), + $msg + ); + } + +} diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php new file mode 100644 index 00000000..c51a1dc5 --- /dev/null +++ b/tests/phpunit/includes/parser/PreprocessorTest.php @@ -0,0 +1,229 @@ +mOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang ); + $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", " Foo " ), + array( "", "" ), + array( "", " <!-- Foo --> " ), + array( " ", " <!-- Foo --> <!-- Bar --> " ), + array( " \n ", " <!-- Foo --> <!-- Bar --> " ), + array( " \n \n", " <!-- Foo --> \n<!-- Bar --> " ), + array( " \n", " <!-- Foo --> \n<!-- Bar -->\n " ), + array( "Bar", " <!-- Foo --> <!-- Bar --> \n" ), + array( "\n== Baz ==\n", " <!-->Bar " ), + array( " == Foo == \n<!-- Bar -->\n == Baz == \n", " " ), + array( "Foo gallery Bar", " Foo " ), + array( "Bar gallery ", " " ), + array( " gallery </gallery> ", " <foo> " ), + array( "gallery </gallery> Foo/foo>", " ", " <foo> " ), + array( "gallery <gallery> </gallery> Foo bar ", "" ), + array( " <noinclude> Foo bar</noinclude> \n{{Foo}}\n ", "" ), + array( " <noinclude> \nFoo \n</noinclude> \n{{Foo}}\n \n", "" ), + array( " <noinclude> \nFoo \n</noinclude> \nfoo bar", " " ), + array( "<{{foo}}>", " gallery foo bar < " ), + array( "<{{{foo}}}>", "foo >< " ), + array( "> foo ", " " ), + array( "=== Foo === ", " gallery </gallery </gallery> " ), + array( "=== Foo === ", " === Foo === " ), + array( "=== Foo === ", " == <!-- --> = Foo ===" ), + array( "=== Foo ===\n", " === Foo == <!-- --> =" ), + array( "=== Foo === \n", " === Foo === \n<!-- --> " ), + array( "== Foo ==\n== Bar == \n", " === Foo === \n<!-- --> <!-- --> " ), + array( "===========", " == Foo == \n== Bar == \n" ), + array( "Foo\n=\n==\n=\n", " =========== Foo\n=\n==\n=\n " ), + array( "{{Foo}}", "" ), + array( "\n{{Foo}}", " Foo \n " ), + array( "{{Foo|bar}}", "Foo " ), + array( "{{Foo|bar}}a", " Foo bar " ), + array( "{{Foo|bar|baz}}", " Foo a bar " ), + array( "{{Foo|1=bar}}", " Foo bar baz " ), + array( "{{Foo|=bar}}", " Foo 1 =bar " ), + array( "{{Foo|bar=baz}}", " Foo = bar " ), + array( "{{Foo|{{bar}}=baz}}", " Foo bar =baz " ), + array( "{{Foo|1=bar|baz}}", " Foo = bar baz " ), + array( "{{Foo|1=bar|2=baz}}", " Foo 1 =bar baz " ), + array( "{{Foo|bar|foo=baz}}", " Foo 1 =bar 2 =baz " ), + array( "{{{1}}}", " Foo bar foo =baz " ), + array( "{{{1|}}}", " 1 " ), + array( "{{{Foo}}}", " 1 " ), + array( "{{{Foo|}}}", " Foo " ), + array( "{{{Foo|bar|baz}}}", " Foo " ), + array( "{{Foo}}", " Foo bar baz { " ), + array( "{{{{Foobar}}}}", "<!-- --> {Foo}}{ " ), + array( "{{{ {{Foo}} }}}", "} Foobar " ), + array( "{{ {{{Foo}}} }}", " Foo " ), + array( "{{{{{Foo}}}}}", " Foo " ), + array( "{{{{{Foo}} }}}", " Foo " ), + array( "{{{{{{Foo}}}}}}", " Foo " ), + array( "{{{{{{Foo}}}}}", " Foo { " ), + array( "[[[Foo]]", "Foo [[[Foo]] " ), + array( "{{Foo|[[[[bar]]|baz]]}}", "" ), // This test is important, since it means the difference between having the [[ rule stacked or not + array( "{{Foo|[[[[bar]|baz]]}}", " Foo [[[[bar]]|baz]] {{Foo|[[[[bar]|baz]]}} " ), + array( "{{Foo|Foo [[[[bar]|baz]]}}", "{{Foo|Foo [[[[bar]|baz]]}} " ), + array( "FooBar Baz", "Foo " ), + array( "FooBaz display map Bar </display map > Bar Baz", "Foo " ), + array( "FooBaz display map foo Bar </display map > ", " Foo " ), + array( "Foogallery bar="baz" ", " Foo " ), + array( "gallery bar="1" baz=2 " ), # Worth blacklisting IMHO + array( "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", " /foo Foo <//foo> " ), + array( "{{#if: {{{1|}}} | Foo | {{Bar}} }}", " #ifexpr: ( = 2) 1 1 Foo Bar " ), + array( "{{#if: {{{1|}}} | Foo | [[Bar]] }}", " #if: 1 Foo Bar " ), + array( "{{#if: {{{1|}}} | [[Foo]] | Bar }}", " #if: 1 Foo [[Bar]] " ), + array( "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", " #if: 1 [[Foo]] Bar " ), + array( "{{ {{Foo}}", " #if: 1 1 #if: 1 2 3 {{ " ), + array( "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "Foo {{Foobar " ), + array( "[[Foo]] |", "Foo Bar Baz [[Foo]] | " ), + array( "{{Foo|Bar|", "{{Foo|Bar| " ), + array( "[[Foo]", "[[Foo] " ), + array( "[[Foo|Bar]", "[[Foo|Bar] " ), + array( "{{Foo| [[Bar] }}", "{{Foo| [[Bar] }} " ), + array( "{{Foo| [[Bar|Baz] }}", "{{Foo| [[Bar|Baz] }} " ), + array( "{{Foo|bar=[[baz]}}", "{{Foo|bar=[[baz]}} " ), + array( "{{foo|", "{{foo| " ), + array( "{{foo|}", "{{foo|} " ), + array( "{{foo|} }}", "" ), + array( "{{foo|bar=|}", " foo } {{foo|bar=|} " ), + array( "{{Foo|} Bar=", "{{Foo|} Bar= " ), + array( "{{Foo|} Bar=}}", "" ), + /* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */ + ); + } + + /** + * Get XML preprocessor tree from the preprocessor (which may not be the + * native XML-based one). + * + * @param string $wikiText + * @return string + */ + function preprocessToXml( $wikiText ) { + if ( method_exists( $this->mPreprocessor, 'preprocessToXml' ) ) { + return $this->normalizeXml( $this->mPreprocessor->preprocessToXml( $wikiText ) ); + } + + $dom = $this->mPreprocessor->preprocessToObj( $wikiText ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + return $dom->saveXML(); + } else { + return $this->normalizeXml( $dom->__toString() ); + } + } + + /** + * Normalize XML string to the form that a DOMDocument saves out. + * + * @param string $xml + * @return string + */ + function normalizeXml( $xml ) { + return preg_replace( '!<([a-z]+)/>!', '<$1>$1>', str_replace( ' />', '/>', $xml ) ); + } + + /** + * @dataProvider provideCases + */ + function testPreprocessorOutput( $wikiText, $expectedXml ) { + $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->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. + array( "NestedTemplates" ), # bug 27936 + ); + } + + /** + * @dataProvider provideFiles + */ + function testPreprocessorOutputFiles( $filename ) { + $folder = __DIR__ . "/../../../parser/preprocess"; + $wikiText = file_get_contents( "$folder/$filename.txt" ); + $output = $this->preprocessToXml( $wikiText ); + + $expectedFilename = "$folder/$filename.expected"; + if ( file_exists( $expectedFilename ) ) { + $expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) ); + $this->assertEquals( $expectedXml, $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 ==", " Foo } Bar =" ), + array( "== h == ", " == h == <!--c1--> " ), + array( "== h == ", " == h == <!--c1--> " ), + array( "== h == ", " == h == <!--c1--> " ), + array( "== h ==", " == h == <!--c1--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> " ), + array( "== h ==", " == h == <!--c1--> <!--c2--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> " ), + + /* These are not working: */ + array( "== h == ", " == h == <!--c1--> <!--c2--> <!--c3--> == h == " ), + array( "== h == ", "<!--c1--> <!--c2--> == h == " ), + array( "== h == ", "<!--c1--> <!--c2--> == h == " ), + array( "== h == x ", "<!--c1--> <!--c2--> == h == x " ), + array( "== h == x ", "<!--c1--> <!--c2--> <!--c3--> == h == " ), + array( "== h == x ", "<!--c1--> x<!--c2--> <!--c3--> == h == " ), + ); + } + + /** + * @dataProvider provideHeadings + */ + function testHeadings( $wikiText, $expectedXml ) { + $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) ); + } +} diff --git a/tests/phpunit/includes/parser/TagHooksTest.php b/tests/phpunit/includes/parser/TagHooksTest.php new file mode 100644 index 00000000..ed600790 --- /dev/null +++ b/tests/phpunit/includes/parser/TagHooksTest.php @@ -0,0 +1,82 @@ +bar" ), array( "foo\nbar" ), array( "foo\rbar" ) ); + } + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( 'wgAlwaysUseTidy', false ); + } + + /** + * @dataProvider provideValidNames + */ + function testTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setHook( $tag, array( $this, 'tagCallback' ) ); + $parserOutput = $parser->parse( "Foo<$tag>Bar$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->assertEquals( "<!--c1--> <!--c2--> <!--c3--> xFooOneBaz\n
", $parserOutput->getText() ); + + $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle + } + + /** + * @dataProvider provideBadNames + * @expectedException MWException + */ + function testBadTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setHook( $tag, array( $this, 'tagCallback' ) ); + $parser->parse( "Foo<$tag>Bar$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->fail( 'Exception not thrown.' ); + } + + /** + * @dataProvider provideValidNames + */ + function testFunctionTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 ); + $parserOutput = $parser->parse( "Foo<$tag>Bar$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->assertEquals( "FooOneBaz\n
", $parserOutput->getText() ); + + $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle + } + + /** + * @dataProvider provideBadNames + * @expectedException MWException + */ + function testBadFunctionTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS ); + $parser->parse( "Foo<$tag>Bar$tag>Baz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $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..6abca6d4 --- /dev/null +++ b/tests/phpunit/includes/search/SearchEngineTest.php @@ -0,0 +1,176 @@ +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 ); + } + + protected function tearDown() { + unset( $this->search ); + + parent::tearDown(); + } + + function pageExists( $title ) { + return false; + } + + function addDBData() { + if ( $this->pageExists( 'Not_Main_Page' ) ) { + return; + } + + if ( !$this->isWikitextNS( NS_MAIN ) ) { + //@todo: cover the case of non-wikitext content in the main namespace + 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 ) { + if ( !$this->isWikitextNS( NS_MAIN ) ) { + $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content " + . "in the main namespace" ); + } + + $this->assertTrue( is_object( $results ) ); + + $matches = array(); + $row = $results->next(); + while ( $row ) { + $matches[] = $row->getTitle()->getPrefixedText(); + $row = $results->next(); + } + $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 ) { + $title = Title::newFromText( $pageName, $ns ); + + $user = User::newFromName( 'WikiSysop' ); + $comment = 'Search Test'; + + // avoid memory leak...? + LinkCache::singleton()->clear(); + + $page = WikiPage::factory( $title ); + $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user ); + + $this->pageList[] = array( $title, $page->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..7d867bc4 --- /dev/null +++ b/tests/phpunit/includes/search/SearchUpdateTest.php @@ -0,0 +1,81 @@ +setMwGlobals( 'wgSearchType', 'MockSearch' ); + } + + 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 testUpdateText() { + $this->assertEquals( + 'test', + $this->updateText( 'TeSt' ), + 'HTML stripped, text lowercased' + ); + + $this->assertEquals( + 'foo bar boz quux', + $this->updateText( <<+ foobar +boz quux + +EOT + ), 'Stripping HTML tables' ); + + $this->assertEquals( + 'a b', + $this->updateText( 'a > b' ), + 'Handle unclosed tags' + ); + + $text = str_pad( "fooassertNotEquals( + '', + $this->updateText( $text ), + 'Bug 18609' + ); + } + + function testBug32712() { + $text = "text „http://example.com“ text"; + $result = $this->updateText( $text ); + $processed = preg_replace( '/Q/u', 'Q', $result ); + $this->assertTrue( + $processed != '', + 'Link surrounded by unicode quotes should not fail UTF-8 validation' + ); + } +} diff --git a/tests/phpunit/includes/site/MediaWikiSiteTest.php b/tests/phpunit/includes/site/MediaWikiSiteTest.php new file mode 100644 index 00000000..0cecdeea --- /dev/null +++ b/tests/phpunit/includes/site/MediaWikiSiteTest.php @@ -0,0 +1,89 @@ + + */ +class MediaWikiSiteTest extends SiteTest { + + public function testNormalizePageTitle() { + $site = new MediaWikiSite(); + $site->setGlobalId( 'enwiki' ); + + //NOTE: this does not actually call out to the enwiki site to perform the normalization, + // but uses a local Title object to do so. This is hardcoded on SiteLink::normalizePageTitle + // for the case that MW_PHPUNIT_TEST is set. + $this->assertEquals( 'Foo', $site->normalizePageName( ' foo ' ) ); + } + + public function fileUrlProvider() { + return array( + // url, filepath, path arg, expected + array( 'https://en.wikipedia.org', '/w/$1', 'api.php', 'https://en.wikipedia.org/w/api.php' ), + array( 'https://en.wikipedia.org', '/w/', 'api.php', 'https://en.wikipedia.org/w/' ), + array( 'https://en.wikipedia.org', '/foo/page.php?name=$1', 'api.php', 'https://en.wikipedia.org/foo/page.php?name=api.php' ), + array( 'https://en.wikipedia.org', '/w/$1', '', 'https://en.wikipedia.org/w/' ), + array( 'https://en.wikipedia.org', '/w/$1', 'foo/bar/api.php', 'https://en.wikipedia.org/w/foo/bar/api.php' ), + ); + } + + /** + * @dataProvider fileUrlProvider + */ + public function testGetFileUrl( $url, $filePath, $pathArgument, $expected ) { + $site = new MediaWikiSite(); + $site->setFilePath( $url . $filePath ); + + $this->assertEquals( $expected, $site->getFileUrl( $pathArgument ) ); + } + + public function provideGetPageUrl() { + return array( + // path, page, expected substring + array( 'http://acme.test/wiki/$1', 'Berlin', '/wiki/Berlin' ), + array( 'http://acme.test/wiki/', 'Berlin', '/wiki/' ), + array( 'http://acme.test/w/index.php?title=$1', 'Berlin', '/w/index.php?title=Berlin' ), + array( 'http://acme.test/wiki/$1', '', '/wiki/' ), + array( 'http://acme.test/wiki/$1', 'Berlin/sub page', '/wiki/Berlin/sub_page' ), + array( 'http://acme.test/wiki/$1', 'Cork (city) ', '/Cork_(city)' ), + array( 'http://acme.test/wiki/$1', 'M&M', '/wiki/M%26M' ), + ); + } + + /** + * @dataProvider provideGetPageUrl + */ + public function testGetPageUrl( $path, $page, $expected ) { + $site = new MediaWikiSite(); + $site->setLinkPath( $path ); + + $this->assertContains( $path, $site->getPageUrl() ); + $this->assertContains( $expected, $site->getPageUrl( $page ) ); + } + +} diff --git a/tests/phpunit/includes/site/SiteListTest.php b/tests/phpunit/includes/site/SiteListTest.php new file mode 100644 index 00000000..c3298397 --- /dev/null +++ b/tests/phpunit/includes/site/SiteListTest.php @@ -0,0 +1,190 @@ + + */ +class SiteListTest extends MediaWikiTestCase { + + /** + * Returns instances of SiteList implementing objects. + * @return array + */ + public function siteListProvider() { + $sitesArrays = $this->siteArrayProvider(); + + $listInstances = array(); + + foreach ( $sitesArrays as $sitesArray ) { + $listInstances[] = new SiteList( $sitesArray[0] ); + } + + return $this->arrayWrap( $listInstances ); + } + + /** + * Returns arrays with instances of Site implementing objects. + * @return array + */ + public function siteArrayProvider() { + $sites = TestSites::getSites(); + + $siteArrays = array(); + + $siteArrays[] = $sites; + + $siteArrays[] = array( array_shift( $sites ) ); + + $siteArrays[] = array( array_shift( $sites ), array_shift( $sites ) ); + + return $this->arrayWrap( $siteArrays ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testIsEmpty( SiteList $sites ) { + $this->assertEquals( count( $sites ) === 0, $sites->isEmpty() ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testGetSiteByGlobalId( SiteList $sites ) { + if ( $sites->isEmpty() ) { + $this->assertTrue( true ); + } else { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertEquals( $site, $sites->getSite( $site->getGlobalId() ) ); + } + } + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testGetSiteByInternalId( $sites ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + if ( is_integer( $site->getInternalId() ) ) { + $this->assertEquals( $site, $sites->getSiteByInternalId( $site->getInternalId() ) ); + } + } + + $this->assertTrue( true ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testHasGlobalId( $sites ) { + $this->assertFalse( $sites->hasSite( 'non-existing-global-id' ) ); + $this->assertFalse( $sites->hasInternalId( 720101010 ) ); + + if ( !$sites->isEmpty() ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) ); + } + } + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testHasInternallId( $sites ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + if ( is_integer( $site->getInternalId() ) ) { + $this->assertTrue( $site, $sites->hasInternalId( $site->getInternalId() ) ); + } + } + + $this->assertFalse( $sites->hasInternalId( -1 ) ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + */ + public function testGetGlobalIdentifiers( SiteList $sites ) { + $identifiers = $sites->getGlobalIdentifiers(); + + $this->assertTrue( is_array( $identifiers ) ); + + $expected = array(); + + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $expected[] = $site->getGlobalId(); + } + + $this->assertArrayEquals( $expected, $identifiers ); + } + + /** + * @dataProvider siteListProvider + * + * @since 1.21 + * + * @param SiteList $list + */ + public function testSerialization( SiteList $list ) { + $serialization = serialize( $list ); + /** + * @var SiteArray $copy + */ + $copy = unserialize( $serialization ); + + $this->assertArrayEquals( $list->getGlobalIdentifiers(), $copy->getGlobalIdentifiers() ); + + /** + * @var Site $site + */ + foreach ( $list as $site ) { + $this->assertTrue( $copy->hasInternalId( $site->getInternalId() ) ); + } + } + +} diff --git a/tests/phpunit/includes/site/SiteSQLStoreTest.php b/tests/phpunit/includes/site/SiteSQLStoreTest.php new file mode 100644 index 00000000..cf4ce945 --- /dev/null +++ b/tests/phpunit/includes/site/SiteSQLStoreTest.php @@ -0,0 +1,123 @@ + + */ +class SiteSQLStoreTest extends MediaWikiTestCase { + + public function testGetSites() { + $expectedSites = TestSites::getSites(); + TestSites::insertIntoDb(); + + $store = SiteSQLStore::newInstance(); + + $sites = $store->getSites(); + + $this->assertInstanceOf( 'SiteList', $sites ); + + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertInstanceOf( 'Site', $site ); + } + + foreach ( $expectedSites as $site ) { + if ( $site->getGlobalId() !== null ) { + $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) ); + } + } + } + + public function testSaveSites() { + $store = SiteSQLStore::newInstance(); + + $sites = array(); + + $site = new Site(); + $site->setGlobalId( 'ertrywuutr' ); + $site->setLanguageCode( 'en' ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'sdfhxujgkfpth' ); + $site->setLanguageCode( 'nl' ); + $sites[] = $site; + + $this->assertTrue( $store->saveSites( $sites ) ); + + $site = $store->getSite( 'ertrywuutr' ); + $this->assertInstanceOf( 'Site', $site ); + $this->assertEquals( 'en', $site->getLanguageCode() ); + $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( $site->getInternalId() >= 0 ); + + $site = $store->getSite( 'sdfhxujgkfpth' ); + $this->assertInstanceOf( 'Site', $site ); + $this->assertEquals( 'nl', $site->getLanguageCode() ); + $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( $site->getInternalId() >= 0 ); + } + + public function testReset() { + $store1 = SiteSQLStore::newInstance(); + $store2 = SiteSQLStore::newInstance(); + + // initialize internal cache + $this->assertGreaterThan( 0, $store1->getSites()->count() ); + $this->assertGreaterThan( 0, $store2->getSites()->count() ); + + // Clear actual data. Will purge the external cache and reset the internal + // cache in $store1, but not the internal cache in store2. + $this->assertTrue( $store1->clear() ); + + // sanity check: $store2 should have a stale cache now + $this->assertNotNull( $store2->getSite( 'enwiki' ) ); + + // purge cache + $store2->reset(); + + // ...now the internal cache of $store2 should be updated and thus empty. + $site = $store2->getSite( 'enwiki' ); + $this->assertNull( $site ); + } + + public function testClear() { + $store = SiteSQLStore::newInstance(); + $this->assertTrue( $store->clear() ); + + $site = $store->getSite( 'enwiki' ); + $this->assertNull( $site ); + + $sites = $store->getSites(); + $this->assertEquals( 0, $sites->count() ); + } + +} diff --git a/tests/phpunit/includes/site/SiteTest.php b/tests/phpunit/includes/site/SiteTest.php new file mode 100644 index 00000000..d20e2a52 --- /dev/null +++ b/tests/phpunit/includes/site/SiteTest.php @@ -0,0 +1,267 @@ + + */ +class SiteTest extends MediaWikiTestCase { + + public function instanceProvider() { + return $this->arrayWrap( TestSites::getSites() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetInterwikiIds( Site $site ) { + $this->assertInternalType( 'array', $site->getInterwikiIds() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetNavigationIds( Site $site ) { + $this->assertInternalType( 'array', $site->getNavigationIds() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testAddNavigationId( Site $site ) { + $site->addNavigationId( 'foobar' ); + $this->assertTrue( in_array( 'foobar', $site->getNavigationIds(), true ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testAddInterwikiId( Site $site ) { + $site->addInterwikiId( 'foobar' ); + $this->assertTrue( in_array( 'foobar', $site->getInterwikiIds(), true ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetLanguageCode( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getLanguageCode(), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testSetLanguageCode( Site $site ) { + $site->setLanguageCode( 'en' ); + $this->assertEquals( 'en', $site->getLanguageCode() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testNormalizePageName( Site $site ) { + $this->assertInternalType( 'string', $site->normalizePageName( 'Foobar' ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetGlobalId( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getGlobalId(), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testSetGlobalId( Site $site ) { + $site->setGlobalId( 'foobar' ); + $this->assertEquals( 'foobar', $site->getGlobalId() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetType( Site $site ) { + $this->assertInternalType( 'string', $site->getType() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetPath( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getPath( 'page_path' ), null ); + $this->assertTypeOrValue( 'string', $site->getPath( 'file_path' ), null ); + $this->assertTypeOrValue( 'string', $site->getPath( 'foobar' ), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testGetAllPaths( Site $site ) { + $this->assertInternalType( 'array', $site->getAllPaths() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testSetAndRemovePath( Site $site ) { + $count = count( $site->getAllPaths() ); + + $site->setPath( 'spam', 'http://www.wikidata.org/$1' ); + $site->setPath( 'spam', 'http://www.wikidata.org/foo/$1' ); + $site->setPath( 'foobar', 'http://www.wikidata.org/bar/$1' ); + + $this->assertEquals( $count + 2, count( $site->getAllPaths() ) ); + + $this->assertInternalType( 'string', $site->getPath( 'foobar' ) ); + $this->assertEquals( 'http://www.wikidata.org/foo/$1', $site->getPath( 'spam' ) ); + + $site->removePath( 'spam' ); + $site->removePath( 'foobar' ); + + $this->assertEquals( $count, count( $site->getAllPaths() ) ); + + $this->assertNull( $site->getPath( 'foobar' ) ); + $this->assertNull( $site->getPath( 'spam' ) ); + } + + public function testSetLinkPath() { + $site = new Site(); + $path = "TestPath/$1"; + + $site->setLinkPath( $path ); + $this->assertEquals( $path, $site->getLinkPath() ); + } + + public function testGetLinkPathType() { + $site = new Site(); + + $path = 'TestPath/$1'; + $site->setLinkPath( $path ); + $this->assertEquals( $path, $site->getPath( $site->getLinkPathType() ) ); + + $path = 'AnotherPath/$1'; + $site->setPath( $site->getLinkPathType(), $path ); + $this->assertEquals( $path, $site->getLinkPath() ); + } + + public function testSetPath() { + $site = new Site(); + + $path = 'TestPath/$1'; + $site->setPath( 'foo', $path ); + + $this->assertEquals( $path, $site->getPath( 'foo' ) ); + } + + public function testProtocolRelativePath() { + $site = new Site(); + + $type = $site->getLinkPathType(); + $path = '//acme.com/'; // protocol-relative URL + $site->setPath( $type, $path ); + + $this->assertEquals( '', $site->getProtocol() ); + } + + public function provideGetPageUrl() { + //NOTE: the assumption that the URL is built by replacing $1 + // with the urlencoded version of $page + // is true for Site but not guaranteed for subclasses. + // Subclasses need to override this provider appropriately. + + return array( + array( #0 + 'http://acme.test/TestPath/$1', + 'Foo', + '/TestPath/Foo', + ), + array( #1 + 'http://acme.test/TestScript?x=$1&y=bla', + 'Foo', + 'TestScript?x=Foo&y=bla', + ), + array( #2 + 'http://acme.test/TestPath/$1', + 'foo & bar/xyzzy (quux-shmoox?)', + '/TestPath/foo%20%26%20bar%2Fxyzzy%20%28quux-shmoox%3F%29', + ), + ); + } + + /** + * @dataProvider provideGetPageUrl + */ + public function testGetPageUrl( $path, $page, $expected ) { + $site = new Site(); + + //NOTE: the assumption that getPageUrl is based on getLinkPath + // is true for Site but not guaranteed for subclasses. + // Subclasses need to override this test case appropriately. + $site->setLinkPath( $path ); + $this->assertContains( $path, $site->getPageUrl() ); + + $this->assertContains( $expected, $site->getPageUrl( $page ) ); + } + + protected function assertTypeOrFalse( $type, $value ) { + if ( $value === false ) { + $this->assertTrue( true ); + } else { + $this->assertInternalType( $type, $value ); + } + } + + /** + * @dataProvider instanceProvider + * @param Site $site + */ + public function testSerialization( Site $site ) { + $this->assertInstanceOf( 'Serializable', $site ); + + $serialization = serialize( $site ); + $newInstance = unserialize( $serialization ); + + $this->assertInstanceOf( 'Site', $newInstance ); + + $this->assertEquals( $serialization, serialize( $newInstance ) ); + } + +} diff --git a/tests/phpunit/includes/site/TestSites.php b/tests/phpunit/includes/site/TestSites.php new file mode 100644 index 00000000..a5656a73 --- /dev/null +++ b/tests/phpunit/includes/site/TestSites.php @@ -0,0 +1,101 @@ + + */ +class TestSites { + + /** + * @since 1.21 + * + * @return array + */ + public static function getSites() { + $sites = array(); + + $site = new Site(); + $site->setGlobalId( 'foobar' ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'enwiktionary' ); + $site->setGroup( 'wiktionary' ); + $site->setLanguageCode( 'en' ); + $site->addNavigationId( 'enwiktionary' ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'dewiktionary' ); + $site->setGroup( 'wiktionary' ); + $site->setLanguageCode( 'de' ); + $site->addInterwikiId( 'dewiktionary' ); + $site->addInterwikiId( 'wiktionaryde' ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://de.wiktionary.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://de.wiktionary.org/w/$1" ); + $sites[] = $site; + + $site = new Site(); + $site->setGlobalId( 'spam' ); + $site->setGroup( 'spam' ); + $site->setLanguageCode( 'en' ); + $site->addNavigationId( 'spam' ); + $site->addNavigationId( 'spamz' ); + $site->addInterwikiId( 'spamzz' ); + $site->setLinkPath( "http://spamzz.test/testing/" ); + $sites[] = $site; + + foreach ( array( 'en', 'de', 'nl', 'sv', 'sr', 'no', 'nn' ) as $langCode ) { + $site = new MediaWikiSite(); + $site->setGlobalId( $langCode . 'wiki' ); + $site->setGroup( 'wikipedia' ); + $site->setLanguageCode( $langCode ); + $site->addInterwikiId( $langCode ); + $site->addNavigationId( $langCode ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://$langCode.wikipedia.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://$langCode.wikipedia.org/w/$1" ); + $sites[] = $site; + } + + return $sites; + } + + /** + * Inserts sites into the database for the unit tests that need them. + * + * @since 0.1 + */ + public static function insertIntoDb() { + $sitesTable = SiteSQLStore::newInstance(); + $sitesTable->clear(); + $sitesTable->saveSites( TestSites::getSites() ); + } + +} diff --git a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php new file mode 100644 index 00000000..3b82e07d --- /dev/null +++ b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php @@ -0,0 +1,79 @@ +manualTest ) ) { + $this->queryPages[$class] = new $class; + } + } + } + + /** + * Test SQL for each of our QueryPages objects + * @group Database + */ + function testQuerypageSqlQuery() { + global $wgDBtype; + + foreach ( $this->queryPages as $page ) { + + // With MySQL, skips special pages reopening a temporary table + // See http://bugs.mysql.com/bug.php?id=10327 + if ( + $wgDBtype === 'mysql' + && in_array( $page->getName(), $this->reopensTempTable ) + ) { + $this->markTestSkipped( "SQL query for page {$page->getName()} can not be tested on MySQL backend (it reopens a temporary table)" ); + continue; + } + + $msg = "SQL query for page {$page->getName()} should give a result wrapper object"; + + $result = $page->reallyDoQuery( 50 ); + if ( $result instanceof ResultWrapper ) { + $this->assertTrue( true, $msg ); + } else { + $this->assertFalse( false, $msg ); + } + } + } +} diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php new file mode 100644 index 00000000..add830b0 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php @@ -0,0 +1,127 @@ +setRequest( new FauxRequest( $requestOptions ) ); + + # setup the rc object + $this->rc = new SpecialRecentChanges(); + $this->rc->setContext( $context ); + $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 + ); + } + + /** 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 static function provideNamespacesAssociations() { + return array( # (NS => Associated_NS) + array( NS_MAIN, NS_TALK ), + array( NS_TALK, NS_MAIN ), + ); + } + +} diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php new file mode 100644 index 00000000..f5ef0fb7 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -0,0 +1,140 @@ + true, 'ns6' => true). NULL to use default options. + * @param $userOptions Array User options to test with. For example array('searchNs5' => 1 );. NULL to use default options. + * @param $expectedProfile An expected search profile name + * @param $expectedNs Array Expected namespaces + */ + function testProfileAndNamespaceLoading( + $requested, $userOptions, $expectedProfile, $expectedNS, + $message = 'Profile name and namespaces mismatches!' + ) { + $context = new RequestContext; + $context->setUser( + $this->newUserWithSearchNS( $userOptions ) + ); + /* + $context->setRequest( new FauxRequest( array( + 'ns5'=>true, + 'ns6'=>true, + ) )); + */ + $context->setRequest( new FauxRequest( $requested ) ); + $search = new SpecialSearch(); + $search->setContext( $context ); + $search->load(); + + /** + * Verify profile name and namespace in the same assertion to make + * sure we will be able to fully compare the above code. PHPUnit stop + * after an assertion fail. + */ + $this->assertEquals( + array( /** Expected: */ + 'ProfileName' => $expectedProfile, + 'Namespaces' => $expectedNS, + ) + , array( /** Actual: */ + 'ProfileName' => $search->getProfile(), + 'Namespaces' => $search->getNamespaces(), + ) + , $message + ); + + } + + function provideSearchOptionsTests() { + $defaultNS = SearchEngine::defaultNamespaces(); + $EMPTY_REQUEST = array(); + $NO_USER_PREF = null; + + return array( + /** + * Parameters: + * , + * Followed by expected values: + * , + * Then an optional message. + */ + array( + $EMPTY_REQUEST, $NO_USER_PREF, + 'default', $defaultNS, + 'Bug 33270: No request nor user preferences should give default profile' + ), + array( + array( 'ns5' => 1 ), $NO_USER_PREF, + 'advanced', array( 5 ), + 'Web request with specific NS should override user preference' + ), + array( + $EMPTY_REQUEST, array( + 'searchNs2' => 1, + 'searchNs14' => 1, + ) + array_fill_keys( array_map( function ( $ns ) { + return "searchNs$ns"; + }, $defaultNS ), 0 ), + 'advanced', array( 2, 14 ), + 'Bug 33583: search with no option should honor User search preferences' + . ' and have all other namespace disabled' + ), + ); + } + + /** + * Helper to create a new User object with given options + * User remains anonymous though + */ + function newUserWithSearchNS( $opt = null ) { + $u = User::newFromId( 0 ); + if ( $opt === null ) { + return $u; + } + foreach ( $opt as $name => $value ) { + $u->setOption( $name, $value ); + } + return $u; + } + + /** + * Verify we do not expand search term in on search result page + * https://gerrit.wikimedia.org/r/4841 + */ + function testSearchTermIsNotExpanded() { + + # Initialize [[Special::Search]] + $search = new SpecialSearch(); + $search->getContext()->setTitle( Title::newFromText( 'Special:Search' ) ); + $search->load(); + + # Simulate a user searching for a given term + $term = '{{SITENAME}}'; + $search->showResults( $term ); + + # Lookup the HTML page title set for that page + $pageTitle = $search + ->getContext() + ->getOutput() + ->getHTMLTitle(); + + # Compare :-] + $this->assertRegExp( + '/' . preg_quote( $term ) . '/', + $pageTitle, + "Search term '{$term}' should not be expanded in Special:Search " + ); + + } +} diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php new file mode 100644 index 00000000..4d2d8ce3 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -0,0 +1,352 @@ +exists() ) { + $this->deleteFile( 'UploadFromUrlTest.png' ); + } + } + + protected function doApiRequest( array $params, array $unused = null, $appendModule = false, User $user = null ) { + $sessionId = session_id(); + session_write_close(); + + $req = new FauxRequest( $params, true, $_SESSION ); + $module = new ApiMain( $req, true ); + $module->execute(); + + wfSetupSession( $sessionId ); + return array( $module->getResultData(), $req ); + } + + /** + * Ensure that the job queue is empty before continuing + */ + public function testClearQueue() { + $job = JobQueueGroup::singleton()->pop(); + while ( $job ) { + $job = JobQueueGroup::singleton()->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->getEditToken(); + $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 = JobQueueGroup::singleton()->pop(); + $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' ); + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testAsyncUpload( $data ) { + $token = $this->user->getEditToken(); + + $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->getEditToken(); + + $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->getEditToken(); + + $job = JobQueueGroup::singleton()->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 = JobQueueGroup::singleton()->pop(); + $this->assertFalse( $job ); + + $this->assertEquals( 'Success', $data[0]['upload']['result'] ); + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + public function testLeaveMessage() { + $token = $this->user->user->getEditToken(); + + $talk = $this->user->user->getTalkPage(); + if ( $talk->exists() ) { + $page = WikiPage::factory( $talk ); + $page->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 = JobQueueGroup::singleton()->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 = JobQueueGroup::singleton()->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 = JobQueueGroup::singleton()->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 ); + $page = WikiPage::factory( $t ); + $page->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..8fcaa214 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadStashTest.php @@ -0,0 +1,77 @@ +bug29408File = __DIR__ . '/bug29408'; + file_put_contents( $this->bug29408File, "\x00" ); + + self::$users = array( + 'sysop' => new TestUser( + 'Uploadstashtestsysop', + 'Upload Stash Test Sysop', + 'upload_stash_test_sysop@example.com', + array( 'sysop' ) + ), + 'uploader' => new TestUser( + 'Uploadstashtestuser', + 'Upload Stash Test User', + 'upload_stash_test_user@example.com', + array() + ) + ); + } + + protected function tearDown() { + if ( file_exists( $this->bug29408File . "." ) ) { + unlink( $this->bug29408File . "." ); + } + + if ( file_exists( $this->bug29408File ) ) { + unlink( $this->bug29408File ); + } + + parent::tearDown(); + } + + 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 testValidRequest() { + $request = new FauxRequest( array( 'wpFileKey' => 'foo' ) ); + $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpFileKey' ); + + $request = new FauxRequest( array( 'wpSessionKey' => 'foo' ) ); + $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpSessionKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpFileKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpSessionKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test', 'wpSessionKey' => 'foo' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check key precedence' ); + } +} diff --git a/tests/phpunit/includes/upload/UploadTest.php b/tests/phpunit/includes/upload/UploadTest.php new file mode 100644 index 00000000..b809d320 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadTest.php @@ -0,0 +1,144 @@ +upload = new UploadTestHandler; + $this->hooks = $wgHooks; + $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) { + return false; + }; + } + + protected function tearDown() { + global $wgHooks; + $wgHooks = $this->hooks; + + parent::tearDown(); + } + + + /** + * First checks the return code + * of UploadBase::getTitle() and then the actual returned title + * + * @dataProvider provideTestTitleValidation + */ + public function testTitleValidation( $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 various forms of valid and invalid titles that can be supplied. + */ + public static function provideTestTitleValidation() { + return array( + /* Test a valid title */ + array( 'ValidTitle.jpg', 'ValidTitle.jpg', UploadBase::OK, + 'upload valid title' ), + /* A title with a slash */ + array( 'A/B.jpg', 'B.jpg', UploadBase::OK, + 'upload title with slash' ), + /* A title with illegal char */ + array( 'A:B.jpg', 'A-B.jpg', UploadBase::OK, + 'upload title with colon' ), + /* Stripping leading File: prefix */ + array( 'File:C.jpg', 'C.jpg', UploadBase::OK, + 'upload title with File prefix' ), + /* Test illegal suggested title (r94601) */ + array( '%281%29.JPG', null, UploadBase::ILLEGAL_FILENAME, + 'illegal title for upload' ), + /* A title without extension */ + array( 'A', null, UploadBase::FILETYPE_MISSING, + 'upload title without extension' ), + /* A title with no basename */ + array( '.jpg', null, UploadBase::MIN_LENGTH_PARTNAME, + 'upload title without basename' ), + /* A title that is longer than 255 bytes */ + array( str_repeat( 'a', 255 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG, + 'upload title longer than 255 bytes' ), + /* A title that is longer than 240 bytes */ + array( str_repeat( 'a', 240 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG, + 'upload title longer than 240 bytes' ), + ); + } + + /** + * 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->mTitleError = UploadBase::OK; + $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..36012748 --- /dev/null +++ b/tests/phpunit/install-phpunit.sh @@ -0,0 +1,37 @@ +#!/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.com + pear update-channels + pear install --alldeps 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/LanguageAmTest.php b/tests/phpunit/languages/LanguageAmTest.php new file mode 100644 index 00000000..9723e1e3 --- /dev/null +++ b/tests/phpunit/languages/LanguageAmTest.php @@ -0,0 +1,25 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageArTest.php b/tests/phpunit/languages/LanguageArTest.php new file mode 100644 index 00000000..523ee7f6 --- /dev/null +++ b/tests/phpunit/languages/LanguageArTest.php @@ -0,0 +1,72 @@ +assertEquals( '١٬٢٣٤٬٥٦٧', $this->getLang()->formatNum( '1234567' ) ); + $this->assertEquals( '-١٢٫٨٩', $this->getLang()->formatNum( -12.89 ) ); + } + + /** + * Mostly to test the raw ascii feature. + * @dataProvider providerSprintfDate + */ + function testSprintfDate( $format, $date, $expected ) { + $this->assertEquals( $expected, $this->getLang()->sprintfDate( $format, $date ) ); + } + + function providerSprintfDate() { + return array( + array( + 'xg "vs" g', + '20120102030410', + 'يناير vs ٣' + ), + array( + 'xmY', + '20120102030410', + '١٤٣٣' + ), + array( + 'xnxmY', + '20120102030410', + '1433' + ), + array( + 'xN xmj xmn xN xmY', + '20120102030410', + ' 7 2 ١٤٣٣' + ), + ); + } + + /** @dataProvider providePlural */ + function testPlural( $result, $value ) { + $forms = array( 'zero', 'one', 'two', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 9 ), + array( 'few', 110 ), + array( 'many', 11 ), + array( 'many', 15 ), + array( 'many', 99 ), + array( 'many', 9999 ), + array( 'other', 100 ), + array( 'other', 102 ), + array( 'other', 1000 ), + array( 'other', 1.7 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBeTest.php b/tests/phpunit/languages/LanguageBeTest.php new file mode 100644 index 00000000..0144941b --- /dev/null +++ b/tests/phpunit/languages/LanguageBeTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBe_taraskTest.php b/tests/phpunit/languages/LanguageBe_taraskTest.php new file mode 100644 index 00000000..5b246d8e --- /dev/null +++ b/tests/phpunit/languages/LanguageBe_taraskTest.php @@ -0,0 +1,73 @@ +assertEquals( 'be-tarask', + $this->getLang()->getCode() + ); + } + + /** see bug 23156 & r64981 */ + function testSearchRightSingleQuotationMarkAsApostroph() { + $this->assertEquals( + "'", + $this->getLang()->normalizeForSearch( '’' ), + 'bug 23156: U+2019 conversion to U+0027' + ); + } + + /** see bug 23156 & r64981 */ + function testCommafy() { + $this->assertEquals( '1,234,567', $this->getLang()->commafy( '1234567' ) ); + $this->assertEquals( '12,345', $this->getLang()->commafy( '12345' ) ); + } + + /** see bug 23156 & r64981 */ + function testDoesNotCommafyFourDigitsNumber() { + $this->assertEquals( '1234', $this->getLang()->commafy( '1234' ) ); + } + + /** @dataProvider providePluralFourForms */ + function testPluralFourForms( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralFourForms() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** @dataProvider providePluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'several' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'several', 11 ), + array( 'several', 91 ), + array( 'several', 121 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageBhoTest.php b/tests/phpunit/languages/LanguageBhoTest.php new file mode 100644 index 00000000..c364917d --- /dev/null +++ b/tests/phpunit/languages/LanguageBhoTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageBsTest.php b/tests/phpunit/languages/LanguageBsTest.php new file mode 100644 index 00000000..76d00704 --- /dev/null +++ b/tests/phpunit/languages/LanguageBsTest.php @@ -0,0 +1,33 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 4 ), + array( 'many', 5 ), + array( 'many', 11 ), + array( 'many', 20 ), + array( 'one', 21 ), + array( 'few', 24 ), + array( 'many', 25 ), + array( 'many', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageClassesTestCase.php b/tests/phpunit/languages/LanguageClassesTestCase.php new file mode 100644 index 00000000..6659dad1 --- /dev/null +++ b/tests/phpunit/languages/LanguageClassesTestCase.php @@ -0,0 +1,100 @@ +getLang() method: + * + * @par Using the crafted language object: + * @code + * function testHasLanguageObject() { + * $langObject = $this->getLang(); + * $this->assertInstanceOf( 'LanguageFoo', + * $langObject + * ); + * } + * @endcode + */ +abstract class LanguageClassesTestCase extends MediaWikiTestCase { + + /** + * Regex used to find out the language code out of the class name + * used by setUpBeforeClass + */ + private static $reExtractLangFromClass = '/Language(.*)Test/'; + + /** + * Hold the language code we are going to use. This is extracted + * directly from the extending class. + */ + private static $LanguageClassCode; + + /** + * Internal language object + * + * A new object is created before each tests thanks to PHPUnit + * setUp() method, it is deleted after each test too. To get + * this object you simply use the getLang method. + * + * You must have setup a language code first. See $LanguageClassCode + * @code + * function testWeAreTheChampions() { + * $this->getLang(); # language object + * } + * @endcode + */ + private $languageObject; + + public static function setUpBeforeClass() { + $found = preg_match( self::$reExtractLangFromClass, + get_called_class(), $m ); + if ( $found ) { + # Normalize language code since classes uses underscores + $m[1] = str_replace( '_', '-', $m[1] ); + } else { + # Fallback to english language + $m[1] = 'en'; + wfDebug( + __METHOD__ . " could not extract a language name " + . "out of " . get_called_class() . " failling back to 'en'\n" + ); + } + // TODO: validate $m[1] which should be a valid language code + self::$LanguageClassCode = $m[1]; + } + + protected function getLang() { + return $this->languageObject; + } + + /** + * Create a new language object before each test. + */ + protected function setUp() { + parent::setUp(); + $this->languageObject = Language::factory( + self::$LanguageClassCode ); + } + + /** + * Delete the internal language object so each test start + * out with a fresh language instance. + */ + protected function tearDown() { + unset( $this->languageObject ); + parent::tearDown(); + } + +} diff --git a/tests/phpunit/languages/LanguageCsTest.php b/tests/phpunit/languages/LanguageCsTest.php new file mode 100644 index 00000000..884a129e --- /dev/null +++ b/tests/phpunit/languages/LanguageCsTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageCuTest.php b/tests/phpunit/languages/LanguageCuTest.php new file mode 100644 index 00000000..e2394b35 --- /dev/null +++ b/tests/phpunit/languages/LanguageCuTest.php @@ -0,0 +1,33 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'many', 3 ), + array( 'many', 4 ), + array( 'other', 5 ), + array( 'one', 11 ), + array( 'other', 20 ), + array( 'few', 22 ), + array( 'many', 223 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageCyTest.php b/tests/phpunit/languages/LanguageCyTest.php new file mode 100644 index 00000000..2a7f4a92 --- /dev/null +++ b/tests/phpunit/languages/LanguageCyTest.php @@ -0,0 +1,34 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'many', 6 ), + array( 'other', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 22 ), + array( 'other', 223 ), + array( 'other', 200.00 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageDsbTest.php b/tests/phpunit/languages/LanguageDsbTest.php new file mode 100644 index 00000000..285ce648 --- /dev/null +++ b/tests/phpunit/languages/LanguageDsbTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'one', 101 ), + array( 'one', 90001 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 203 ), + array( 'few', 4 ), + array( 'other', 99 ), + array( 'other', 555 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageFrTest.php b/tests/phpunit/languages/LanguageFrTest.php new file mode 100644 index 00000000..faf0de58 --- /dev/null +++ b/tests/phpunit/languages/LanguageFrTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageGaTest.php b/tests/phpunit/languages/LanguageGaTest.php new file mode 100644 index 00000000..2dbb088b --- /dev/null +++ b/tests/phpunit/languages/LanguageGaTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageGdTest.php b/tests/phpunit/languages/LanguageGdTest.php new file mode 100644 index 00000000..5de1e9d2 --- /dev/null +++ b/tests/phpunit/languages/LanguageGdTest.php @@ -0,0 +1,48 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array ( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'one', 11 ), + array( 'two', 12 ), + array( 'few', 3 ), + array( 'few', 19 ), + array( 'other', 200 ), + ); + } + + /** @dataProvider providerPluralExplicit */ + function testExplicitPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other', '11=Form11', '12=Form12' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralExplicit() { + return array ( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'Form11', 11 ), + array( 'Form12', 12 ), + array( 'few', 3 ), + array( 'few', 19 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageGvTest.php b/tests/phpunit/languages/LanguageGvTest.php new file mode 100644 index 00000000..4126e071 --- /dev/null +++ b/tests/phpunit/languages/LanguageGvTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'Form 4', 0 ), + array( 'Form 2', 1 ), + array( 'Form 3', 2 ), + array( 'Form 4', 3 ), + array( 'Form 1', 20 ), + array( 'Form 2', 21 ), + array( 'Form 3', 22 ), + array( 'Form 4', 23 ), + array( 'Form 4', 50 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageHeTest.php b/tests/phpunit/languages/LanguageHeTest.php new file mode 100644 index 00000000..6de88e59 --- /dev/null +++ b/tests/phpunit/languages/LanguageHeTest.php @@ -0,0 +1,77 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralDual() { + return array( + array( 'other', 0 ), // Zero -> plural + array( 'one', 1 ), // Singular + array( 'two', 2 ), // Dual + array( 'other', 3 ), // Plural + ); + } + + /** @dataProvider providerPlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), // Zero -> plural + array( 'one', 1 ), // Singular + array( 'other', 2 ), // Plural, no dual provided + array( 'other', 3 ), // Plural + ); + } + + /** @dataProvider providerGrammar */ + function testGrammar( $result, $word, $case ) { + $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) ); + } + + // The comments in the beginning of the line help avoid RTL problems + // with text editors. + function providerGrammar() { + return array( + array( + /* result */ 'וויקיפדיה', + /* word */ 'ויקיפדיה', + /* case */ 'תחילית', + ), + array( + /* result */ 'וולפגנג', + /* word */ 'וולפגנג', + /* case */ 'prefixed', + ), + array( + /* result */ 'קובץ', + /* word */ 'הקובץ', + /* case */ 'תחילית', + ), + array( + /* result */ '־Wikipedia', + /* word */ 'Wikipedia', + /* case */ 'תחילית', + ), + array( + /* result */ '־1995', + /* word */ '1995', + /* case */ 'תחילית', + ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHiTest.php b/tests/phpunit/languages/LanguageHiTest.php new file mode 100644 index 00000000..86d6af58 --- /dev/null +++ b/tests/phpunit/languages/LanguageHiTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageHrTest.php b/tests/phpunit/languages/LanguageHrTest.php new file mode 100644 index 00000000..9dce4ea7 --- /dev/null +++ b/tests/phpunit/languages/LanguageHrTest.php @@ -0,0 +1,33 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 4 ), + array( 'many', 5 ), + array( 'many', 11 ), + array( 'many', 20 ), + array( 'one', 21 ), + array( 'few', 24 ), + array( 'many', 25 ), + array( 'many', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageHsbTest.php b/tests/phpunit/languages/LanguageHsbTest.php new file mode 100644 index 00000000..bec7d819 --- /dev/null +++ b/tests/phpunit/languages/LanguageHsbTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'one', 101 ), + array( 'one', 90001 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 203 ), + array( 'few', 4 ), + array( 'other', 99 ), + array( 'other', 555 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageHuTest.php b/tests/phpunit/languages/LanguageHuTest.php new file mode 100644 index 00000000..23d8e0ce --- /dev/null +++ b/tests/phpunit/languages/LanguageHuTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageHyTest.php b/tests/phpunit/languages/LanguageHyTest.php new file mode 100644 index 00000000..7088d37b --- /dev/null +++ b/tests/phpunit/languages/LanguageHyTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageKshTest.php b/tests/phpunit/languages/LanguageKshTest.php new file mode 100644 index 00000000..9b4a53ad --- /dev/null +++ b/tests/phpunit/languages/LanguageKshTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageLnTest.php b/tests/phpunit/languages/LanguageLnTest.php new file mode 100644 index 00000000..669d8b0a --- /dev/null +++ b/tests/phpunit/languages/LanguageLnTest.php @@ -0,0 +1,26 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageLtTest.php b/tests/phpunit/languages/LanguageLtTest.php new file mode 100644 index 00000000..9d6428b8 --- /dev/null +++ b/tests/phpunit/languages/LanguageLtTest.php @@ -0,0 +1,45 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** @dataProvider provideOneFewCases */ + function testOneFewPlural( $result, $value ) { + $forms = array( 'one', 'few' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function provideOneFewOtherCases() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 9 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'one', 21 ), + array( 'few', 32 ), + array( 'one', 41 ), + array( 'one', 40001 ), + ); + } + + function provideOneFewCases() { + return array( + array( 'one', 1 ), + array( 'few', 15 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageLvTest.php b/tests/phpunit/languages/LanguageLvTest.php new file mode 100644 index 00000000..bd0c759b --- /dev/null +++ b/tests/phpunit/languages/LanguageLvTest.php @@ -0,0 +1,31 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'other', 11 ), + array( 'one', 21 ), + array( 'other', 411 ), + array( 'other', 12.345 ), + array( 'other', 20 ), + array( 'one', 31 ), + array( 'other', 200 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageMgTest.php b/tests/phpunit/languages/LanguageMgTest.php new file mode 100644 index 00000000..c1e516bc --- /dev/null +++ b/tests/phpunit/languages/LanguageMgTest.php @@ -0,0 +1,27 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + array( 'other', 123.3434 ), + ); + } + +} diff --git a/tests/phpunit/languages/LanguageMkTest.php b/tests/phpunit/languages/LanguageMkTest.php new file mode 100644 index 00000000..5c241ba7 --- /dev/null +++ b/tests/phpunit/languages/LanguageMkTest.php @@ -0,0 +1,33 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 11 ), + array( 'one', 21 ), + array( 'other', 411 ), + array( 'other', 12.345 ), + array( 'other', 20 ), + array( 'one', 31 ), + array( 'other', 200 ), + ); + } + + +} diff --git a/tests/phpunit/languages/LanguageMlTest.php b/tests/phpunit/languages/LanguageMlTest.php new file mode 100644 index 00000000..396114d9 --- /dev/null +++ b/tests/phpunit/languages/LanguageMlTest.php @@ -0,0 +1,35 @@ +assertEquals( $result, $this->getLang()->formatNum( $value ) ); + } + + function providerFormatNum() { + return array( + array( '12,34,567', '1234567' ), + array( '12,345', '12345' ), + array( '1', '1' ), + array( '123', '123' ), + array( '1,234', '1234' ), + array( '12,345.56', '12345.56' ), + array( '12,34,56,79,81,23,45,678', '12345679812345678' ), + array( '.12345', '.12345' ), + array( '-12,00,000', '-1200000' ), + array( '-98', '-98' ), + array( '-98', -98 ), + array( '-1,23,45,678', -12345678 ), + array( '', '' ), + array( '', null ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMoTest.php b/tests/phpunit/languages/LanguageMoTest.php new file mode 100644 index 00000000..f7da1cd6 --- /dev/null +++ b/tests/phpunit/languages/LanguageMoTest.php @@ -0,0 +1,35 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'few', 101 ), + array( 'few', 119 ), + array( 'other', 120 ), + array( 'other', 200 ), + array( 'few', 201 ), + array( 'few', 219 ), + array( 'other', 220 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMtTest.php b/tests/phpunit/languages/LanguageMtTest.php new file mode 100644 index 00000000..f2b881e7 --- /dev/null +++ b/tests/phpunit/languages/LanguageMtTest.php @@ -0,0 +1,64 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralAllForms() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 10 ), + array( 'many', 11 ), + array( 'many', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'other', 101 ), + array( 'few', 102 ), + array( 'few', 110 ), + array( 'many', 111 ), + array( 'many', 119 ), + array( 'other', 120 ), + array( 'other', 201 ), + ); + } + + /** @dataProvider providerPluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'many' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralTwoForms() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + array( 'many', 10 ), + array( 'many', 11 ), + array( 'many', 19 ), + array( 'many', 20 ), + array( 'many', 99 ), + array( 'many', 100 ), + array( 'many', 101 ), + array( 'many', 102 ), + array( 'many', 110 ), + array( 'many', 111 ), + array( 'many', 119 ), + array( 'many', 120 ), + array( 'many', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageNlTest.php b/tests/phpunit/languages/LanguageNlTest.php new file mode 100644 index 00000000..f783f2c0 --- /dev/null +++ b/tests/phpunit/languages/LanguageNlTest.php @@ -0,0 +1,20 @@ +assertEquals( '1.234.567', $this->getLang()->formatNum( '1234567' ) ); + $this->assertEquals( '12.345', $this->getLang()->formatNum( '12345' ) ); + $this->assertEquals( '1', $this->getLang()->formatNum( '1' ) ); + $this->assertEquals( '123', $this->getLang()->formatNum( '123' ) ); + $this->assertEquals( '1.234', $this->getLang()->formatNum( '1234' ) ); + $this->assertEquals( '12.345,56', $this->getLang()->formatNum( '12345.56' ) ); + $this->assertEquals( ',1234556', $this->getLang()->formatNum( '.1234556' ) ); + } +} diff --git a/tests/phpunit/languages/LanguageNsoTest.php b/tests/phpunit/languages/LanguageNsoTest.php new file mode 100644 index 00000000..9d80d138 --- /dev/null +++ b/tests/phpunit/languages/LanguageNsoTest.php @@ -0,0 +1,24 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguagePlTest.php b/tests/phpunit/languages/LanguagePlTest.php new file mode 100644 index 00000000..1e36097b --- /dev/null +++ b/tests/phpunit/languages/LanguagePlTest.php @@ -0,0 +1,64 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralFourForms() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'many', 5 ), + array( 'many', 9 ), + array( 'many', 10 ), + array( 'many', 11 ), + array( 'many', 21 ), + array( 'few', 22 ), + array( 'few', 23 ), + array( 'few', 24 ), + array( 'many', 25 ), + array( 'many', 200 ), + array( 'many', 201 ), + ); + } + + /** @dataProvider providerPlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'many' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + array( 'many', 3 ), + array( 'many', 4 ), + array( 'many', 5 ), + array( 'many', 9 ), + array( 'many', 10 ), + array( 'many', 11 ), + array( 'many', 21 ), + array( 'many', 22 ), + array( 'many', 23 ), + array( 'many', 24 ), + array( 'many', 25 ), + array( 'many', 200 ), + array( 'many', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageRoTest.php b/tests/phpunit/languages/LanguageRoTest.php new file mode 100644 index 00000000..916ea45d --- /dev/null +++ b/tests/phpunit/languages/LanguageRoTest.php @@ -0,0 +1,35 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'few', 101 ), + array( 'few', 119 ), + array( 'other', 120 ), + array( 'other', 200 ), + array( 'few', 201 ), + array( 'few', 219 ), + array( 'other', 220 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageRuTest.php b/tests/phpunit/languages/LanguageRuTest.php new file mode 100644 index 00000000..0792f75b --- /dev/null +++ b/tests/phpunit/languages/LanguageRuTest.php @@ -0,0 +1,78 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralFourForms() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** @dataProvider providePluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'several' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'several', 11 ), + array( 'several', 91 ), + array( 'several', 121 ), + ); + } + + /** @dataProvider providerGrammar */ + function testGrammar( $result, $word, $case ) { + $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) ); + } + + function providerGrammar() { + return array( + array( + 'Википедии', + 'Википедия', + 'genitive', + ), + array( + 'Викитеки', + 'Викитека', + 'genitive', + ), + array( + 'Викитеке', + 'Викитека', + 'prepositional', + ), + array( + 'Викиданных', + 'Викиданные', + 'prepositional', + ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSeTest.php b/tests/phpunit/languages/LanguageSeTest.php new file mode 100644 index 00000000..c7dd8020 --- /dev/null +++ b/tests/phpunit/languages/LanguageSeTest.php @@ -0,0 +1,40 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralThreeForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + ); + } + + /** @dataProvider providerPlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSgsTest.php b/tests/phpunit/languages/LanguageSgsTest.php new file mode 100644 index 00000000..95e63462 --- /dev/null +++ b/tests/phpunit/languages/LanguageSgsTest.php @@ -0,0 +1,58 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralAllForms() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + array( 'few', 10 ), + array( 'few', 11 ), + array( 'few', 12 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'few', 100 ), + array( 'one', 101 ), + array( 'few', 111 ), + array( 'few', 112 ), + ); + } + + /** @dataProvider providePluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 12 ), + array( 'other', 19 ), + array( 'other', 20 ), + array( 'other', 100 ), + array( 'one', 101 ), + array( 'other', 111 ), + array( 'other', 112 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageShTest.php b/tests/phpunit/languages/LanguageShTest.php new file mode 100644 index 00000000..282fd2f2 --- /dev/null +++ b/tests/phpunit/languages/LanguageShTest.php @@ -0,0 +1,24 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSkTest.php b/tests/phpunit/languages/LanguageSkTest.php new file mode 100644 index 00000000..89cbbf01 --- /dev/null +++ b/tests/phpunit/languages/LanguageSkTest.php @@ -0,0 +1,32 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSlTest.php b/tests/phpunit/languages/LanguageSlTest.php new file mode 100644 index 00000000..075e6af3 --- /dev/null +++ b/tests/phpunit/languages/LanguageSlTest.php @@ -0,0 +1,34 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'one', 101 ), + array( 'two', 102 ), + array( 'few', 103 ), + array( 'one', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSmaTest.php b/tests/phpunit/languages/LanguageSmaTest.php new file mode 100644 index 00000000..6d655219 --- /dev/null +++ b/tests/phpunit/languages/LanguageSmaTest.php @@ -0,0 +1,40 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPluralThreeForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + ); + } + + /** @dataProvider providerPlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSrTest.php b/tests/phpunit/languages/LanguageSrTest.php new file mode 100644 index 00000000..5611030b --- /dev/null +++ b/tests/phpunit/languages/LanguageSrTest.php @@ -0,0 +1,219 @@ + + * @copyright Copyright © 2011, Antoine Musso + * @file + */ + +require_once dirname( __DIR__ ) . '/bootstrap.php'; + +/** Tests for MediaWiki languages/LanguageSr.php */ +class LanguageSrTest extends LanguageClassesTestCase { + + ##### TESTS ####################################################### + + function testEasyConversions() { + $this->assertCyrillic( + 'шђчћжШЂЧЋЖ', + 'Cyrillic guessing characters' + ); + $this->assertLatin( + 'šđč枊ĐČĆŽ', + 'Latin guessing characters' + ); + } + + function testMixedConversions() { + $this->assertCyrillic( + 'шђчћжШЂЧЋЖ - šđčćž', + 'Mostly cyrillic characters' + ); + $this->assertLatin( + 'šđč枊ĐČĆŽ - шђчћж', + 'Mostly latin characters' + ); + } + + function testSameAmountOfLatinAndCyrillicGetConverted() { + $this->assertConverted( + '4 latin: šđčć | 4 cyrillic: шђчћ', + 'sr-ec' + ); + $this->assertConverted( + '4 latin: šđčć | 4 cyrillic: шђчћ', + 'sr-el' + ); + } + + /** + * @author Nikola Smolenski + */ + function testConversionToCyrillic() { + //A simple convertion of Latin to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'abvg' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vg-{dž}-' ) + ); + //A simple convertion of Cyrillic to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'абвг' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{dž}-' ) + ); + //This text has some Latin, but is recognized as Cyrillic, so it should not be converted + $this->assertEquals( 'abvgшђжчћ', + $this->convertToCyrillic( 'abvgшђжчћ' ) + ); + //Same as above, but assert that -{}-s must be removed + $this->assertEquals( 'љabvgњшђжчћџ', + $this->convertToCyrillic( '-{љ}-abvg-{њ}-шђжчћ-{џ}-' ) + ); + //This text has some Cyrillic, but is recognized as Latin, so it should be converted + $this->assertEquals( 'абвгшђжчћ', + $this->convertToCyrillic( 'абвгšđžčć' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабвгnjшђжчћdž', + $this->convertToCyrillic( '-{lj}-абвг-{nj}-šđžčć-{dž}-' ) + ); + // Roman numerals are not converted + $this->assertEquals( 'а I б II в III г IV шђжчћ', + $this->convertToCyrillic( 'a I b II v III g IV šđžčć' ) + ); + } + + function testConversionToLatin() { + //A simple convertion of Latin to Latin + $this->assertEquals( 'abcd', + $this->convertToLatin( 'abcd' ) + ); + //A simple convertion of Cyrillic to Latin + $this->assertEquals( 'abcd', + $this->convertToLatin( 'абцд' ) + ); + //This text has some Latin, but is recognized as Cyrillic, so it should be converted + $this->assertEquals( 'abcdšđžčć', + $this->convertToLatin( 'abcdшђжчћ' ) + ); + //This text has some Cyrillic, but is recognized as Latin, so it should not be converted + $this->assertEquals( 'абцдšđžčć', + $this->convertToLatin( 'абцдšđžčć' ) + ); + } + + /** @dataProvider providePluralFourForms */ + function testPluralFourForms( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralFourForms() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** @dataProvider providePluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'several' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'several', 11 ), + array( 'several', 91 ), + array( 'several', 121 ), + ); + } + + ##### HELPERS ##################################################### + /** + *Wrapper to verify text stay the same after applying conversion + * @param $text string Text to convert + * @param $variant string Language variant 'sr-ec' or 'sr-el' + * @param $msg string Optional message + */ + function assertUnConverted( $text, $variant, $msg = '' ) { + $this->assertEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Wrapper to verify a text is different once converted to a variant. + * @param $text string Text to convert + * @param $variant string Language variant 'sr-ec' or 'sr-el' + * @param $msg string Optional message + */ + function assertConverted( $text, $variant, $msg = '' ) { + $this->assertNotEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Verifiy the given Cyrillic text is not converted when using + * using the cyrillic variant and converted to Latin when using + * the Latin variant. + */ + function assertCyrillic( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'sr-ec', $msg ); + $this->assertConverted( $text, 'sr-el', $msg ); + } + + /** + * Verifiy the given Latin text is not converted when using + * using the Latin variant and converted to Cyrillic when using + * the Cyrillic variant. + */ + function assertLatin( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'sr-el', $msg ); + $this->assertConverted( $text, 'sr-ec', $msg ); + } + + + /** Wrapper for converter::convertTo() method*/ + function convertTo( $text, $variant ) { + return $this->getLang() + ->mConverter + ->convertTo( + $text, $variant + ); + } + + function convertToCyrillic( $text ) { + return $this->convertTo( $text, 'sr-ec' ); + } + + function convertToLatin( $text ) { + return $this->convertTo( $text, 'sr-el' ); + } +} diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php new file mode 100644 index 00000000..76471445 --- /dev/null +++ b/tests/phpunit/languages/LanguageTest.php @@ -0,0 +1,1352 @@ +assertEquals( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + $this->getLang()->normalizeForSearch( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ), + 'convertDoubleWidth() with the full alphabet and digits' + ); + } + + /** + * @dataProvider provideFormattableTimes + */ + function testFormatTimePeriod( $seconds, $format, $expected, $desc ) { + $this->assertEquals( $expected, $this->getLang()->formatTimePeriod( $seconds, $format ), $desc ); + } + + function provideFormattableTimes() { + return array( + array( + 9.45, + array(), + '9.5 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.45, + array( 'noabbrevs' => true ), + '9.5 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array(), + '10 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array( 'noabbrevs' => true ), + '10 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 59.55, + array(), + '1 min 0 s', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 59.55, + array( 'noabbrevs' => true ), + '1 minute 0 seconds', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 119.55, + array(), + '2 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 119.55, + array( 'noabbrevs' => true ), + '2 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array(), + '1 h 0 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array( 'noabbrevs' => true ), + '1 hour 0 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 7199.55, + array(), + '2 h 0 min 0 s', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + array( 'noabbrevs' => true ), + '2 hours 0 minutes 0 seconds', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + 'avoidseconds', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + 'avoidminutes', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 7199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 172799.55, + 'avoidseconds', + '48 h 0 min', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 172799.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '48 hours 0 minutes', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 259199.55, + 'avoidminutes', + '3 d 0 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '3 days 0 hours', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + 'avoidseconds', + '2 d 1 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 1 hour 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + 'avoidminutes', + '2 d 1 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 days 1 hour', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + 'avoidseconds', + '3 d 0 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 259199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '3 days 0 hours 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 172801.55, + 'avoidseconds', + '2 d 0 h 0 min', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 172801.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 0 hours 0 minutes', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 176460.55, + array(), + '2 d 1 h 1 min 1 s', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), + array( + 176460.55, + array( 'noabbrevs' => true ), + '2 days 1 hour 1 minute 1 second', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), + ); + + } + + function testTruncate() { + $this->assertEquals( + "XXX", + $this->getLang()->truncate( "1234567890", 0, 'XXX' ), + 'truncate prefix, len 0, small ellipsis' + ); + + $this->assertEquals( + "12345XXX", + $this->getLang()->truncate( "1234567890", 8, 'XXX' ), + 'truncate prefix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->getLang()->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ), + 'truncate prefix, large ellipsis' + ); + + $this->assertEquals( + "XXX67890", + $this->getLang()->truncate( "1234567890", -8, 'XXX' ), + 'truncate suffix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->getLang()->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ), + 'truncate suffix, large ellipsis' + ); + } + + /** + * @dataProvider provideHTMLTruncateData() + */ + function testTruncateHtml( $len, $ellipsis, $input, $expected ) { + // Actual HTML... + $this->assertEquals( + $expected, + $this->getLang()->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, '***', + ' ', + '
', + ), + array( 2, '***', + '
123456789
', + '***
', + ), + array( 2, '***', + '23456789
', + '***
', + ), + array( 3, '***', + '123456789
', + '***
', + ), + array( 4, '***', + '123456789
', + '1***
', + ), + array( 5, '***', + '123456789', + '12***', + ), + array( 6, '***', + '', + '', + ), + array( 6, '***', + '', + '', + ), + array( 7, '***', + '123456
789', + '1234***
', + ), + array( 8, '***', + '123456789', + '12345***', + ), + array( 9, '***', + '', + '
123456789 ', + ), + array( 10, '***', + '
123456789 123456789
', + '123456789
', + ), + ); + } + + /** + * Test Language::isWellFormedLanguageTag() + * @dataProvider provideWellFormedLanguageTags + */ + function testWellFormedLanguageTag( $code, $message = '' ) { + $this->assertTrue( + Language::isWellFormedLanguageTag( $code ), + "validating code $code $message" + ); + } + + /** + * The test cases are based on the tests in the GaBuZoMeu parser + * written by Stéphane Bortzmeyer+ * and distributed as free software, under the GNU General Public Licence. + * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html + */ + function provideWellFormedLanguageTags() { + return array( + array( 'fr', 'two-letter code' ), + array( 'fr-latn', 'two-letter code with lower case script code' ), + array( 'fr-Latn-FR', 'two-letter code with title case script code and uppercase country code' ), + array( 'fr-Latn-419', 'two-letter code with title case script code and region number' ), + array( 'fr-FR', 'two-letter code with uppercase' ), + array( 'ax-TZ', 'Not in the registry, but well-formed' ), + array( 'fr-shadok', 'two-letter code with variant' ), + array( 'fr-y-myext-myext2', 'non-x singleton' ), + array( 'fra-Latn', 'ISO 639 can be 3-letters' ), + array( 'fra', 'three-letter language code' ), + array( 'fra-FX', 'three-letter language code with country code' ), + array( 'i-klingon', 'grandfathered with singleton' ), + array( 'I-kLINgon', 'tags are case-insensitive...' ), + array( 'no-bok', 'grandfathered without singleton' ), + array( 'i-enochian', 'Grandfathered' ), + array( 'x-fr-CH', 'private use' ), + array( 'es-419', 'two-letter code with region number' ), + array( 'en-Latn-GB-boont-r-extended-sequence-x-private', 'weird, but well-formed' ), + array( 'ab-x-abc-x-abc', 'anything goes after x' ), + array( 'ab-x-abc-a-a', 'anything goes after x, including several non-x singletons' ), + array( 'i-default', 'grandfathered' ), + array( 'abcd-Latn', 'Language of 4 chars reserved for future use' ), + array( 'AaBbCcDd-x-y-any-x', 'Language of 5-8 chars, registered' ), + array( 'de-CH-1901', 'with country and year' ), + array( 'en-US-x-twain', 'with country and singleton' ), + array( 'zh-cmn', 'three-letter variant' ), + array( 'zh-cmn-Hant', 'three-letter variant and script' ), + array( 'zh-cmn-Hant-HK', 'three-letter variant, script and country' ), + array( 'xr-p-lze', 'Extension' ), + ); + } + + /** + * Negative test for Language::isWellFormedLanguageTag() + * @dataProvider provideMalformedLanguageTags + */ + function testMalformedLanguageTag( $code, $message = '' ) { + $this->assertFalse( + Language::isWellFormedLanguageTag( $code ), + "validating that code $code is a malformed language tag - $message" + ); + } + + /** + * The test cases are based on the tests in the GaBuZoMeu parser + * written by Stéphane Bortzmeyer + * and distributed as free software, under the GNU General Public Licence. + * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html + */ + function provideMalformedLanguageTags() { + return array( + array( 'f', 'language too short' ), + array( 'f-Latn', 'language too short with script' ), + array( 'xr-lxs-qut', 'variants too short' ), # extlangS + array( 'fr-Latn-F', 'region too short' ), + array( 'a-value', 'language too short with region' ), + array( 'tlh-a-b-foo', 'valid three-letter with wrong variant' ), + array( 'i-notexist', 'grandfathered but not registered: invalid, even if we only test well-formedness' ), + array( 'abcdefghi-012345678', 'numbers too long' ), + array( 'ab-abc-abc-abc-abc', 'invalid extensions' ), + array( 'ab-abcd-abc', 'invalid extensions' ), + array( 'ab-ab-abc', 'invalid extensions' ), + array( 'ab-123-abc', 'invalid extensions' ), + array( 'a-Hant-ZH', 'short language with valid extensions' ), + array( 'a1-Hant-ZH', 'invalid character in language' ), + array( 'ab-abcde-abc', 'invalid extensions' ), + array( 'ab-1abc-abc', 'invalid characters in extensions' ), + array( 'ab-ab-abcd', 'invalid order of extensions' ), + array( 'ab-123-abcd', 'invalid order of extensions' ), + array( 'ab-abcde-abcd', 'invalid extensions' ), + array( 'ab-1abc-abcd', 'invalid characters in extensions' ), + array( 'ab-a-b', 'extensions too short' ), + array( 'ab-a-x', 'extensions too short, even with singleton' ), + array( 'ab--ab', 'two separators' ), + array( 'ab-abc-', 'separator in the end' ), + array( '-ab-abc', 'separator in the beginning' ), + array( 'abcd-efg', 'language too long' ), + array( 'aabbccddE', 'tag too long' ), + array( 'pa_guru', 'A tag with underscore is invalid in strict mode' ), + array( 'de-f', 'subtag too short' ), + ); + } + + /** + * Negative test for Language::isWellFormedLanguageTag() + */ + function testLenientLanguageTag() { + $this->assertTrue( + Language::isWellFormedLanguageTag( 'pa_guru', true ), + 'pa_guru is a well-formed language tag in lenient mode' + ); + } + + /** + * 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)' ), + ); + } + + /** + * Test Language::isKnownLanguageTag() + * @dataProvider provideKnownLanguageTags + */ + function testKnownLanguageTag( $code, $message = '' ) { + $this->assertTrue( + (bool)Language::isKnownLanguageTag( $code ), + "validating code $code - $message" + ); + } + + function provideKnownLanguageTags() { + return array( + array( 'fr', 'simple code' ), + array( 'bat-smg', 'an MW legacy tag' ), + array( 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ), + ); + } + + /** + * Test Language::isKnownLanguageTag() + */ + function testKnownCldrLanguageTag() { + if ( !class_exists( 'LanguageNames' ) ) { + $this->markTestSkipped( 'The LanguageNames class is not available. The cldr extension is probably not installed.' ); + } + + $this->assertTrue( + (bool)Language::isKnownLanguageTag( 'pal' ), + 'validating code "pal" an ancient language, which probably will not appear in Names.php, but appears in CLDR in English' + ); + } + + /** + * Negative tests for Language::isKnownLanguageTag() + * @dataProvider provideUnKnownLanguageTags + */ + function testUnknownLanguageTag( $code, $message = '' ) { + $this->assertFalse( + (bool)Language::isKnownLanguageTag( $code ), + "checking that code $code is invalid - $message" + ); + } + + function provideUnknownLanguageTags() { + return array( + array( 'mw', 'non-existent two-letter code' ), + ); + } + + /** + * @dataProvider provideSprintfDateSamples + */ + function testSprintfDate( $format, $ts, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->sprintfDate( $format, $ts ), + "sprintfDate('$format', '$ts'): $msg" + ); + } + + /** + * bug 33454. sprintfDate should always use UTC. + * @dataProvider provideSprintfDateSamples + */ + function testSprintfDateTZ( $format, $ts, $expected, $msg ) { + $oldTZ = date_default_timezone_get(); + $res = date_default_timezone_set( 'Asia/Seoul' ); + if ( !$res ) { + $this->markTestSkipped( "Error setting Timezone" ); + } + + $this->assertEquals( + $expected, + $this->getLang()->sprintfDate( $format, $ts ), + "sprintfDate('$format', '$ts'): $msg" + ); + + date_default_timezone_set( $oldTZ ); + } + + function provideSprintfDateSamples() { + return array( + array( + 'xiY', + '20111212000000', + '1390', // note because we're testing English locale we get Latin-standard digits + 'Iranian calendar full year' + ), + array( + 'xiy', + '20111212000000', + '90', + 'Iranian calendar short year' + ), + array( + 'o', + '20120101235000', + '2011', + 'ISO 8601 (week) year' + ), + array( + 'W', + '20120101235000', + '52', + 'Week number' + ), + array( + 'W', + '20120102235000', + '1', + 'Week number' + ), + array( + 'o-\\WW-N', + '20091231235000', + '2009-W53-4', + 'leap week' + ), + // What follows is mostly copied from http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time + array( + 'Y', + '20120102090705', + '2012', + 'Full year' + ), + array( + 'y', + '20120102090705', + '12', + '2 digit year' + ), + array( + 'L', + '20120102090705', + '1', + 'Leap year' + ), + array( + 'n', + '20120102090705', + '1', + 'Month index, not zero pad' + ), + array( + 'N', + '20120102090705', + '01', + 'Month index. Zero pad' + ), + array( + 'M', + '20120102090705', + 'Jan', + 'Month abbrev' + ), + array( + 'F', + '20120102090705', + 'January', + 'Full month' + ), + array( + 'xg', + '20120102090705', + 'January', + 'Genitive month name (same in EN)' + ), + array( + 'j', + '20120102090705', + '2', + 'Day of month (not zero pad)' + ), + array( + 'd', + '20120102090705', + '02', + 'Day of month (zero-pad)' + ), + array( + 'z', + '20120102090705', + '1', + 'Day of year (zero-indexed)' + ), + array( + 'D', + '20120102090705', + 'Mon', + 'Day of week (abbrev)' + ), + array( + 'l', + '20120102090705', + 'Monday', + 'Full day of week' + ), + array( + 'N', + '20120101090705', + '7', + 'Day of week (Mon=1, Sun=7)' + ), + array( + 'w', + '20120101090705', + '0', + 'Day of week (Sun=0, Sat=6)' + ), + array( + 'N', + '20120102090705', + '1', + 'Day of week' + ), + array( + 'a', + '20120102090705', + 'am', + 'am vs pm' + ), + array( + 'A', + '20120102120000', + 'PM', + 'AM vs PM' + ), + array( + 'a', + '20120102000000', + 'am', + 'AM vs PM' + ), + array( + 'g', + '20120102090705', + '9', + '12 hour, not Zero' + ), + array( + 'h', + '20120102090705', + '09', + '12 hour, zero padded' + ), + array( + 'G', + '20120102090705', + '9', + '24 hour, not zero' + ), + array( + 'H', + '20120102090705', + '09', + '24 hour, zero' + ), + array( + 'H', + '20120102110705', + '11', + '24 hour, zero' + ), + array( + 'i', + '20120102090705', + '07', + 'Minutes' + ), + array( + 's', + '20120102090705', + '05', + 'seconds' + ), + array( + 'U', + '20120102090705', + '1325495225', + 'unix time' + ), + array( + 't', + '20120102090705', + '31', + 'Days in current month' + ), + array( + 'c', + '20120102090705', + '2012-01-02T09:07:05+00:00', + 'ISO 8601 timestamp' + ), + array( + 'r', + '20120102090705', + 'Mon, 02 Jan 2012 09:07:05 +0000', + 'RFC 5322' + ), + array( + 'xmj xmF xmn xmY', + '20120102090705', + '7 Safar 2 1433', + 'Islamic' + ), + array( + 'xij xiF xin xiY', + '20120102090705', + '12 Dey 10 1390', + 'Iranian' + ), + array( + 'xjj xjF xjn xjY', + '20120102090705', + '7 Tevet 4 5772', + 'Hebrew' + ), + array( + 'xjt', + '20120102090705', + '29', + 'Hebrew number of days in month' + ), + array( + 'xjx', + '20120102090705', + 'Tevet', + 'Hebrew genitive month name (No difference in EN)' + ), + array( + 'xkY', + '20120102090705', + '2555', + 'Thai year' + ), + array( + 'xoY', + '20120102090705', + '101', + 'Minguo' + ), + array( + 'xtY', + '20120102090705', + '平成24', + 'nengo' + ), + array( + 'xrxkYY', + '20120102090705', + 'MMDLV2012', + 'Roman numerals' + ), + array( + 'xhxjYY', + '20120102090705', + 'ה\'תשע"ב2012', + 'Hebrew numberals' + ), + array( + 'xnY', + '20120102090705', + '2012', + 'Raw numerals (doesn\'t mean much in EN)' + ), + array( + '[[Y "(yea"\\r)]] \\"xx\\"', + '20120102090705', + '[[2012 (year)]] "x"', + 'Various escaping' + ), + + ); + } + + /** + * @dataProvider provideFormatSizes + */ + function testFormatSize( $size, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->formatSize( $size ), + "formatSize('$size'): $msg" + ); + } + + function provideFormatSizes() { + return array( + array( + 0, + "0 B", + "Zero bytes" + ), + array( + 1024, + "1 KB", + "1 kilobyte" + ), + array( + 1024 * 1024, + "1 MB", + "1,024 megabytes" + ), + array( + 1024 * 1024 * 1024, + "1 GB", + "1 gigabytes" + ), + array( + pow( 1024, 4 ), + "1 TB", + "1 terabyte" + ), + array( + pow( 1024, 5 ), + "1 PB", + "1 petabyte" + ), + array( + pow( 1024, 6 ), + "1 EB", + "1,024 exabyte" + ), + array( + pow( 1024, 7 ), + "1 ZB", + "1 zetabyte" + ), + array( + pow( 1024, 8 ), + "1 YB", + "1 yottabyte" + ), + // How big!? THIS BIG! + ); + } + + /** + * @dataProvider provideFormatBitrate + */ + function testFormatBitrate( $bps, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->formatBitrate( $bps ), + "formatBitrate('$bps'): $msg" + ); + } + + function provideFormatBitrate() { + return array( + array( + 0, + "0 bps", + "0 bits per second" + ), + array( + 999, + "999 bps", + "999 bits per second" + ), + array( + 1000, + "1 kbps", + "1 kilobit per second" + ), + array( + 1000 * 1000, + "1 Mbps", + "1 megabit per second" + ), + array( + pow( 10, 9 ), + "1 Gbps", + "1 gigabit per second" + ), + array( + pow( 10, 12 ), + "1 Tbps", + "1 terabit per second" + ), + array( + pow( 10, 15 ), + "1 Pbps", + "1 petabit per second" + ), + array( + pow( 10, 18 ), + "1 Ebps", + "1 exabit per second" + ), + array( + pow( 10, 21 ), + "1 Zbps", + "1 zetabit per second" + ), + array( + pow( 10, 24 ), + "1 Ybps", + "1 yottabit per second" + ), + array( + pow( 10, 27 ), + "1,000 Ybps", + "1,000 yottabits per second" + ), + ); + } + + + /** + * @dataProvider provideFormatDuration + */ + function testFormatDuration( $duration, $expected, $intervals = array() ) { + $this->assertEquals( + $expected, + $this->getLang()->formatDuration( $duration, $intervals ), + "formatDuration('$duration'): $expected" + ); + } + + function provideFormatDuration() { + return array( + array( + 0, + '0 seconds', + ), + array( + 1, + '1 second', + ), + array( + 2, + '2 seconds', + ), + array( + 60, + '1 minute', + ), + array( + 2 * 60, + '2 minutes', + ), + array( + 3600, + '1 hour', + ), + array( + 2 * 3600, + '2 hours', + ), + array( + 24 * 3600, + '1 day', + ), + array( + 2 * 86400, + '2 days', + ), + array( + // ( 365 + ( 24 * 3 + 25 ) / 400 ) * 86400 = 31556952 + ( 365 + ( 24 * 3 + 25 ) / 400.0 ) * 86400, + '1 year', + ), + array( + 2 * 31556952, + '2 years', + ), + array( + 10 * 31556952, + '1 decade', + ), + array( + 20 * 31556952, + '2 decades', + ), + array( + 100 * 31556952, + '1 century', + ), + array( + 200 * 31556952, + '2 centuries', + ), + array( + 1000 * 31556952, + '1 millennium', + ), + array( + 2000 * 31556952, + '2 millennia', + ), + array( + 9001, + '2 hours, 30 minutes and 1 second' + ), + array( + 3601, + '1 hour and 1 second' + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days, 2 hours and 30 minutes' + ), + array( + 42 * 1000 * 31556952 + 42, + '42 millennia and 42 seconds' + ), + array( + 60, + '60 seconds', + array( 'seconds' ), + ), + array( + 61, + '61 seconds', + array( 'seconds' ), + ), + array( + 1, + '1 second', + array( 'seconds' ), + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'years', 'days', 'minutes' ), + ), + array( + 42, + '0 days', + array( 'years', 'days' ), + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'minutes', 'days', 'years' ), + ), + array( + 42, + '0 days', + array( 'days', 'years' ), + ), + ); + } + + /** + * @dataProvider provideCheckTitleEncodingData + */ + function testCheckTitleEncoding( $s ) { + $this->assertEquals( + $s, + $this->getLang()->checkTitleEncoding( $s ), + "checkTitleEncoding('$s')" + ); + } + + function provideCheckTitleEncodingData() { + return array( + array( "" ), + array( "United States of America" ), // 7bit ASCII + array( rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ), + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn" + ) + ), + // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for + // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding + // uses mb_check_encoding for its test. + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C" + . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C" + . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C" + . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C" + . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C" + . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C" + . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C" + . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C" + . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C" + . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C" + . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C" + . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C" + . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C" + . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis" + ), + ), + array( + rawurldecode( + "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C" + . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C" + . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C" + . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C" + . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou" + . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C" + . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C" + . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C" + . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C" + . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C" + . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C" + . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes" + ) + ) + ); + } + + /** + * @dataProvider provideRomanNumeralsData + */ + function testRomanNumerals( $num, $numerals ) { + $this->assertEquals( + $numerals, + Language::romanNumeral( $num ), + "romanNumeral('$num')" + ); + } + + function provideRomanNumeralsData() { + return array( + array( 1, 'I' ), + array( 2, 'II' ), + array( 3, 'III' ), + array( 4, 'IV' ), + array( 5, 'V' ), + array( 6, 'VI' ), + array( 7, 'VII' ), + array( 8, 'VIII' ), + array( 9, 'IX' ), + array( 10, 'X' ), + array( 20, 'XX' ), + array( 30, 'XXX' ), + array( 40, 'XL' ), + array( 49, 'XLIX' ), + array( 50, 'L' ), + array( 60, 'LX' ), + array( 70, 'LXX' ), + array( 80, 'LXXX' ), + array( 90, 'XC' ), + array( 99, 'XCIX' ), + array( 100, 'C' ), + array( 200, 'CC' ), + array( 300, 'CCC' ), + array( 400, 'CD' ), + array( 500, 'D' ), + array( 600, 'DC' ), + array( 700, 'DCC' ), + array( 800, 'DCCC' ), + array( 900, 'CM' ), + array( 999, 'CMXCIX' ), + array( 1000, 'M' ), + array( 1989, 'MCMLXXXIX' ), + array( 2000, 'MM' ), + array( 3000, 'MMM' ), + array( 4000, 'MMMM' ), + array( 5000, 'MMMMM' ), + array( 6000, 'MMMMMM' ), + array( 7000, 'MMMMMMM' ), + array( 8000, 'MMMMMMMM' ), + array( 9000, 'MMMMMMMMM' ), + array( 9999, 'MMMMMMMMMCMXCIX' ), + array( 10000, 'MMMMMMMMMM' ), + ); + } + + /** + * @dataProvider providePluralData + */ + function testConvertPlural( $expected, $number, $forms ) { + $chosen = $this->getLang()->convertPlural( $number, $forms ); + $this->assertEquals( $expected, $chosen ); + } + + function providePluralData() { + // Params are: [expected text, number given, [the plural forms]] + return array( + array( 'plural', 0, array( + 'singular', 'plural' + ) ), + array( 'explicit zero', 0, array( + '0=explicit zero', 'singular', 'plural' + ) ), + array( 'explicit one', 1, array( + 'singular', 'plural', '1=explicit one', + ) ), + array( 'singular', 1, array( + 'singular', 'plural', '0=explicit zero', + ) ), + array( 'plural', 3, array( + '0=explicit zero', '1=explicit one', 'singular', 'plural' + ) ), + array( 'explicit eleven', 11, array( + 'singular', 'plural', '11=explicit eleven', + ) ), + array( 'plural', 12, array( + 'singular', 'plural', '11=explicit twelve', + ) ), + array( 'plural', 12, array( + 'singular', 'plural', '=explicit form', + ) ), + array( 'other', 2, array( + 'kissa=kala', '1=2=3', 'other', + ) ), + ); + } + + /** + * @covers Language::translateBlockExpiry() + * @dataProvider provideTranslateBlockExpiry + */ + function testTranslateBlockExpiry( $expectedData, $str, $desc ) { + $lang = $this->getLang(); + if ( is_array( $expectedData ) ) { + list( $func, $arg ) = $expectedData; + $expected = $lang->$func( $arg ); + } else { + $expected = $expectedData; + } + $this->assertEquals( $expected, $lang->translateBlockExpiry( $str ), $desc ); + } + + function provideTranslateBlockExpiry() { + return array( + array( '2 hours', '2 hours', 'simple data from ipboptions' ), + array( 'indefinite', 'infinite', 'infinite from ipboptions' ), + array( 'indefinite', 'infinity', 'alternative infinite from ipboptions' ), + array( 'indefinite', 'indefinite', 'another alternative infinite from ipboptions' ), + array( array( 'formatDuration', 1023 * 60 * 60 ), '1023 hours', 'relative' ), + array( array( 'formatDuration', -1023 ), '-1023 seconds', 'negative relative' ), + array( array( 'formatDuration', 0 ), 'now', 'now' ), + array( array( 'timeanddate', '20120102070000' ), '2012-1-1 7:00 +1 day', 'mixed, handled as absolute' ), + array( array( 'timeanddate', '19910203040506' ), '1991-2-3 4:05:06', 'absolute' ), + array( array( 'timeanddate', '19700101000000' ), '1970-1-1 0:00:00', 'absolute at epoch' ), + array( array( 'timeanddate', '19691231235959' ), '1969-12-31 23:59:59', 'time before epoch' ), + array( 'dummy', 'dummy', 'return garbage as is' ), + ); + } + + /** + * @covers Language::commafy() + * @dataProvider provideCommafyData + */ + function testCommafy( $number, $numbersWithCommas ) { + $this->assertEquals( + $numbersWithCommas, + $this->getLang()->commafy( $number ), + "commafy('$number')" + ); + } + + function provideCommafyData() { + return array( + array( 1, '1' ), + array( 10, '10' ), + array( 100, '100' ), + array( 1000, '1,000' ), + array( 10000, '10,000' ), + array( 100000, '100,000' ), + array( 1000000, '1,000,000' ), + array( 1.0001, '1.0001' ), + array( 10.0001, '10.0001' ), + array( 100.0001, '100.0001' ), + array( 1000.0001, '1,000.0001' ), + array( 10000.0001, '10,000.0001' ), + array( 100000.0001, '100,000.0001' ), + array( 1000000.0001, '1,000,000.0001' ), + ); + } + + function testListToText() { + $lang = $this->getLang(); + $and = $lang->getMessageFromDB( 'and' ); + $s = $lang->getMessageFromDB( 'word-separator' ); + $c = $lang->getMessageFromDB( 'comma-separator' ); + + $this->assertEquals( '', $lang->listToText( array() ) ); + $this->assertEquals( 'a', $lang->listToText( array( 'a' ) ) ); + $this->assertEquals( "a{$and}{$s}b", $lang->listToText( array( 'a', 'b' ) ) ); + $this->assertEquals( "a{$c}b{$and}{$s}c", $lang->listToText( array( 'a', 'b', 'c' ) ) ); + $this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( array( 'a', 'b', 'c', 'd' ) ) ); + } + + /** + * @dataProvider provideIsSupportedLanguage + */ + function testIsSupportedLanguage( $code, $expected, $comment ) { + $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment ); + } + + static function provideIsSupportedLanguage() { + return array( + array( 'en', true, 'is supported language' ), + array( 'fi', true, 'is supported language' ), + array( 'bunny', false, 'is not supported language' ), + array( 'FI', false, 'is not supported language, input should be in lower case' ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTiTest.php b/tests/phpunit/languages/LanguageTiTest.php new file mode 100644 index 00000000..8af0eee2 --- /dev/null +++ b/tests/phpunit/languages/LanguageTiTest.php @@ -0,0 +1,24 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTlTest.php b/tests/phpunit/languages/LanguageTlTest.php new file mode 100644 index 00000000..abd8581a --- /dev/null +++ b/tests/phpunit/languages/LanguageTlTest.php @@ -0,0 +1,24 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTrTest.php b/tests/phpunit/languages/LanguageTrTest.php new file mode 100644 index 00000000..e93d49d9 --- /dev/null +++ b/tests/phpunit/languages/LanguageTrTest.php @@ -0,0 +1,60 @@ +getLang()->ucfirst( $input ); + } elseif ( $func == 'lcfirst' ) { + $res = $this->getLang()->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/languages/LanguageUkTest.php b/tests/phpunit/languages/LanguageUkTest.php new file mode 100644 index 00000000..9bbfaf66 --- /dev/null +++ b/tests/phpunit/languages/LanguageUkTest.php @@ -0,0 +1,48 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralFourForms() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** @dataProvider providePluralTwoForms */ + function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'several' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'several', 11 ), + array( 'several', 91 ), + array( 'several', 121 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageUzTest.php b/tests/phpunit/languages/LanguageUzTest.php new file mode 100644 index 00000000..495c0be6 --- /dev/null +++ b/tests/phpunit/languages/LanguageUzTest.php @@ -0,0 +1,115 @@ + + * @copyright Copyright © 2012, Robin Pepermans + * @copyright Copyright © 2011, Antoine Musso + * @file + */ + +require_once dirname( __DIR__ ) . '/bootstrap.php'; + +/** Tests for MediaWiki languages/LanguageUz.php */ +class LanguageUzTest extends LanguageClassesTestCase { + + /** + * @author Nikola Smolenski + */ + function testConversionToCyrillic() { + // A convertion of Latin to Cyrillic + $this->assertEquals( 'абвгғ', + $this->convertToCyrillic( 'abvggʻ' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгўоdb', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vgoʻo-{db}-' ) + ); + // A simple convertion of Cyrillic to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'абвг' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdaž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{da}-ž' ) + ); + } + + function testConversionToLatin() { + // A simple convertion of Latin to Latin + $this->assertEquals( 'abdef', + $this->convertToLatin( 'abdef' ) + ); + // A convertion of Cyrillic to Latin + $this->assertEquals( 'gʻabtsdOʻQyo', + $this->convertToLatin( 'ғабцдЎҚё' ) + ); + } + + ##### HELPERS ##################################################### + /** + * Wrapper to verify text stay the same after applying conversion + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + function assertUnConverted( $text, $variant, $msg = '' ) { + $this->assertEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Wrapper to verify a text is different once converted to a variant. + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + function assertConverted( $text, $variant, $msg = '' ) { + $this->assertNotEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Verifiy the given Cyrillic text is not converted when using + * using the cyrillic variant and converted to Latin when using + * the Latin variant. + */ + function assertCyrillic( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-cyrl', $msg ); + $this->assertConverted( $text, 'uz-latn', $msg ); + } + + /** + * Verifiy the given Latin text is not converted when using + * using the Latin variant and converted to Cyrillic when using + * the Cyrillic variant. + */ + function assertLatin( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-latn', $msg ); + $this->assertConverted( $text, 'uz-cyrl', $msg ); + } + + + /** Wrapper for converter::convertTo() method*/ + function convertTo( $text, $variant ) { + return $this->getLang()->mConverter->convertTo( $text, $variant ); + } + + function convertToCyrillic( $text ) { + return $this->convertTo( $text, 'uz-cyrl' ); + } + + function convertToLatin( $text ) { + return $this->convertTo( $text, 'uz-latn' ); + } +} diff --git a/tests/phpunit/languages/LanguageWaTest.php b/tests/phpunit/languages/LanguageWaTest.php new file mode 100644 index 00000000..28329fa3 --- /dev/null +++ b/tests/phpunit/languages/LanguageWaTest.php @@ -0,0 +1,24 @@ +assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + function providerPlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'many', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php new file mode 100644 index 00000000..73d5dcc0 --- /dev/null +++ b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php @@ -0,0 +1,95 @@ +assertEquals( $expected, $result, $comment ); + } + + /** + * @dataProvider invalidTestCases + * @expectedException CLDRPluralRuleError + */ + function testInvalidRules( $rules, $comment ) { + CLDRPluralRuleEvaluator::evaluate( 1, (array)$rules ); + } + + function validTestCases() { + $tests = array( + # expected, number, rule, comment + array( 0, 'n is 1', 1, 'integer number and is' ), + array( 0, 'n is 1', "1", 'string integer number and is' ), + array( 0, 'n is 1', 1.0, 'float number and is' ), + array( 0, 'n is 1', "1.0", 'string float number and is' ), + array( 1, 'n is 1', 1.1, 'float number and is' ), + array( 1, 'n is 1', 2, 'float number and is' ), + + array( 0, 'n in 1,3,5', 3, '' ), + array( 1, 'n not in 1,3,5', 5, '' ), + + array( 1, 'n in 1,3,5', 2, '' ), + array( 0, 'n not in 1,3,5', 4, '' ), + + array( 0, 'n in 1..3', 2, '' ), + array( 0, 'n in 1..3', 3, 'in is inclusive' ), + array( 1, 'n in 1..3', 0, '' ), + + array( 1, 'n not in 1..3', 2, '' ), + array( 1, 'n not in 1..3', 3, 'in is inclusive' ), + array( 0, 'n not in 1..3', 0, '' ), + + array( 1, 'n is not 1 and n is not 2 and n is not 3', 1, 'and relation' ), + array( 0, 'n is not 1 and n is not 2 and n is not 4', 3, 'and relation' ), + + array( 0, 'n is not 1 or n is 1', 1, 'or relation' ), + array( 1, 'n is 1 or n is 2', 3, 'or relation' ), + + array( 0, 'n is 1', 1, 'extra whitespace' ), + + array( 0, 'n mod 3 is 1', 7, 'mod' ), + array( 0, 'n mod 3 is not 1', 4.3, 'mod with floats' ), + + array( 0, 'n within 1..3', 2, 'within with integer' ), + array( 0, 'n within 1..3', 2.5, 'within with float' ), + array( 0, 'n in 1..3', 2, 'in with integer' ), + array( 1, 'n in 1..3', 2.5, 'in with float' ), + + array( 0, 'n in 3 or n is 4 and n is 5', 3, 'and binds more tightly than or' ), + array( 1, 'n is 3 or n is 4 and n is 5', 4, 'and binds more tightly than or' ), + + array( 0, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 24, 'breton rule' ), + array( 1, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 25, 'breton rule' ), + + array( 0, 'n within 0..2 and n is not 2', 0, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1.2, 'french rule' ), + array( 1, 'n within 0..2 and n is not 2', 2, 'french rule' ), + + array( 1, 'n in 3..10,13..19', 2, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 4, 'scottish rule - ranges with comma' ), + array( 1, 'n in 3..10,13..19', 12.999, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 13, 'scottish rule - ranges with comma' ), + + array( 0, '5 mod 3 is n', 2, 'n as result of mod - no need to pass' ), + ); + + return $tests; + } + + function invalidTestCases() { + $tests = array( + array( 'n mod mod 5 is 1', 'mod mod' ), + array( 'n', 'just n' ), + array( 'n is in 5', 'is in' ), + ); + return $tests; + } + +} diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php new file mode 100644 index 00000000..40d24fc5 --- /dev/null +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,377 @@ +doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $revision_id = $revision->getId(); + $text_id = $revision->getTextId(); + if ( ( $revision_id > 0 ) && ( $text_id > 0 ) ) { + return array( $revision_id, $text_id ); + } + } + throw new MWException( "Could not determine revision id (" . $status->getWikiText() . ")" ); + } + + + /** + * gunzips the given file and stores the result in the original file name + * + * @param $fname string: filename to read the gzipped data from and stored + * the gunzipped data into + */ + protected function gunzip( $fname ) { + $gzipped_contents = file_get_contents( $fname ); + if ( $gzipped_contents === false ) { + $this->fail( "Could not get contents of $fname" ); + } + // We resort to use gzinflate instead of gzdecode, as gzdecode + // need not be available + $contents = gzinflate( substr( $gzipped_contents, 10, -8 ) ); + $this->assertEquals( strlen( $contents ), + file_put_contents( $fname, $contents ), "# bytes written" ); + } + + /** + * Default set up function. + * + * Clears $wgUser, and reports errors from addDBData to PHPUnit + */ + protected function setUp() { + global $wgUser; + + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + // @see self::exceptionFromAddDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $wgUser = new User(); + } + + /** + * Checks for test output consisting only of lines containing ETA announcements + */ + function expectETAOutput() { + // Newer PHPUnits require assertion about the output using PHPUnit's own + // expectOutput[...] functions. However, the PHPUnit shipped prediactes + // do not allow to check /each/ line of the output using /readable/ REs. + // So we ... + // + // 1. ... add a dummy output checking to make PHPUnit not complain + // about unchecked test output + $this->expectOutputRegex( '//' ); + + // 2. Do the real output checking on our own. + $lines = explode( "\n", $this->getActualOutput() ); + $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" ); + $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" ); + $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]"; + foreach ( $lines as $line ) { + $this->assertRegExp( "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/", $line ); + } + } + + + /** + * Step the current XML reader until node end of given name is found. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for ) + * + * @return bool: true iff the end node could be found. false otherwise. + */ + protected function skipToNodeEnd( $name ) { + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::END_ELEMENT && + $this->xml->name == $name + ) { + return true; + } + } + return false; + } + + /** + * Step the current XML reader to the first element start after the node + * end of a given name. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for ) + * + * @return bool: true iff new element after the closing of $name could be + * found. false otherwise. + */ + protected function skipPastNodeEnd( $name ) { + $this->assertTrue( $this->skipToNodeEnd( $name ), + "Skipping to end of $name" ); + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::ELEMENT ) { + return true; + } + } + return false; + } + + /** + * Opens an XML file to analyze and optionally skips past siteinfo. + * + * @param $fname string: name of file to analyze + * @param $skip_siteinfo bool: (optional) If true, step the xml reader + * to the first element after + */ + protected function assertDumpStart( $fname, $skip_siteinfo = true ) { + $this->xml = new XMLReader(); + $this->assertTrue( $this->xml->open( $fname ), + "Opening temporary file $fname via XMLReader failed" ); + if ( $skip_siteinfo ) { + $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ), + "Skipping past end of siteinfo" ); + } + } + + /** + * Asserts that the xml reader is at the final closing tag of an xml file and + * closes the reader. + * + * @param $tag string: (optional) the name of the final tag + * (e.g.: "mediawiki" for ) + */ + protected function assertDumpEnd( $name = "mediawiki" ) { + $this->assertNodeEnd( $name, false ); + if ( $this->xml->read() ) { + $this->skipWhitespace(); + } + $this->assertEquals( $this->xml->nodeType, XMLReader::NONE, + "No proper entity left to parse" ); + $this->xml->close(); + } + + /** + * Steps the xml reader over white space + */ + protected function skipWhitespace() { + $cont = true; + while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) + || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { + $cont = $this->xml->read(); + } + } + + /** + * Asserts that the xml reader is at an element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for ) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeStart( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past start tag" ); + } + } + + /** + * Asserts that the xml reader is at an closing element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the closing element to check for + * (e.g.: "mediawiki" for ) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeEnd( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past end tag" ); + } + } + + + /** + * Asserts that the xml reader is at an element of given tag that contains a given text, + * and skips over the element. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for... ) + * @param $text string|false: If string, check if it equals the elements text. + * If false, ignore the element's text + * @param $skip_ws bool: (optional) if true, skip past white spaces that trail the + * closing element. + */ + protected function assertTextNode( $name, $text, $skip_ws = true ) { + $this->assertNodeStart( $name ); + + if ( $text !== false ) { + $this->assertEquals( $text, $this->xml->value, "Text of node " . $name ); + } + $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); + $this->assertNodeEnd( $name ); + + if ( $skip_ws ) { + $this->skipWhitespace(); + } + } + + /** + * Asserts that the xml reader is at the start of a page element and skips over the first + * tags, after checking them. + * + * Besides the opening page element, this function also checks for and skips over the + * title, ns, and id tags. Hence after this function, the xml reader is at the first + * revision of the current page. + * + * @param $id int: id of the page to assert + * @param $ns int: number of namespage to assert + * @param $name string: title of the current page + */ + protected function assertPageStart( $id, $ns, $name ) { + + $this->assertNodeStart( "page" ); + $this->skipWhitespace(); + + $this->assertTextNode( "title", $name ); + $this->assertTextNode( "ns", $ns ); + $this->assertTextNode( "id", $id ); + + } + + /** + * Asserts that the xml reader is at the page's closing element and skips to the next + * element. + */ + protected function assertPageEnd() { + $this->assertNodeEnd( "page" ); + $this->skipWhitespace(); + } + + /** + * Asserts that the xml reader is at a revision and checks its representation before + * skipping over it. + * + * @param $id int: id of the revision + * @param $summary string: summary of the revision + * @param $text_id int: id of the revision's text + * @param $text_bytes int: # of bytes in the revision's text + * @param $text_sha1 string: the base36 SHA-1 of the revision's text + * @param $text string|false: (optional) The revision's string, or false to check for a + * revision stub + * @param $model String: the expected content model id (default: CONTENT_MODEL_WIKITEXT) + * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT) + * @param $parentid int|false: (optional) id of the parent revision + */ + protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false, + $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) { + + $this->assertNodeStart( "revision" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + if ( $parentid !== false ) { + $this->assertTextNode( "parentid", $parentid ); + } + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "ip", false ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + $this->assertTextNode( "comment", $summary ); + $this->skipWhitespace(); + + if ( $this->xml->name == "text" ) { + // note:tag may occur here or at the very end. + $text_found = true; + $this->assertText( $id, $text_id, $text_bytes, $text ); + } else { + $text_found = false; + } + + $this->assertTextNode( "sha1", $text_sha1 ); + + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + if ( !$text_found ) { + $this->assertText( $id, $text_id, $text_bytes, $text ); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + + protected function assertText( $id, $text_id, $text_bytes, $text ) { + $this->assertNodeStart( "text", false ); + if ( $text_bytes !== false ) { + $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, + "Attribute 'bytes' of revision " . $id ); + } + + if ( $text === false ) { + // Testing for a stub + $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id, + "Text id of revision " . $id ); + $this->assertFalse( $this->xml->hasValue, "Revision has text" ); + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) + && ( $this->xml->name == "text" ) + ) { + + $this->xml->read(); + } + $this->skipWhitespace(); + } else { + // Testing for a real dump + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id ); + $this->assertTrue( $this->xml->read(), "Skipping past text" ); + $this->assertNodeEnd( "text" ); + $this->skipWhitespace(); + } + } +} diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..741f8b7f --- /dev/null +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,820 @@ +shutdownSimulated ) { + $this->testCase->fail( __METHOD__ . " called more than once" ); + } + + // The cleanup action. + $this->outputChanneled( false ); + + // Bookkeeping that we simulated the clean up. + $this->shutdownSimulated = true; + } + + // Note that the "public" here does not change visibility + public function outputChanneled( $msg, $channel = null ) { + if ( $this->shutdownSimulated ) { + if ( $msg !== false ) { + $this->testCase->fail( "Already past simulated shutdown, but msg is " + . "not false. Did the hack in Maintenance.php change? Please " + . "adapt the test case or Maintenance.php" ); + } + + // The current call is the one registered via register_shutdown_function. + // We can safely ignore it, as we simulated this one via simulateShutdown + // before (if we did not, the destructor of this instance will warn about + // it) + return; + } + + return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + /** + * Safety net around register_shutdown_function of Maintenance.php + */ + public function __destruct() { + if ( !$this->shutdownSimulated ) { + // Someone generated a MaintenanceFixup instance without calling + // simulateShutdown. We'd have to raise a PHPUnit exception to correctly + // flag this illegal usage. However, we are already in a destruktor, which + // would trigger undefined behavior. Hence, we can only report to the + // error output :( Hopefully people read the PHPUnit output. + $name = $this->testCase->getName(); + fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " for test $name " + . "destructed without calling simulateShutdown method. Call " + . "simulateShutdown on the instance before it gets destructed." ); + } + + // The following guard is required, as PHP does not offer default destructors :( + if ( is_callable( "parent::__destruct" ) ) { + parent::__destruct(); + } + } + + public function __construct( MediaWikiTestCase $testCase ) { + parent::__construct(); + $this->testCase = $testCase; + } + + + // --- Making protected functions visible for test + + public function output( $out, $channel = null ) { + // Just to make PHP not nag about signature mismatches, we copied + // Maintenance::output signature. However, we do not use (or rely on) + // those variables. Instead we pass to Maintenance::output whatever we + // receive at runtime. + return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + + // --- Requirements for getting instance of abstract class + + public function execute() { + $this->testCase->fail( __METHOD__ . " called unexpectedly" ); + } + +} + +class MaintenanceTest extends MediaWikiTestCase { + + + /** + * The main Maintenance instance that is used for testing. + * + * @var MaintenanceFixup + */ + private $m; + + + protected function setUp() { + parent::setUp(); + $this->m = new MaintenanceFixup( $this ); + } + + protected function tearDown() { + if ( $this->m ) { + $this->m->simulateShutdown(); + $this->m = null; + } + parent::tearDown(); + } + + + /** + * asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::m. + * + * @param $preShutdownOutput string: expected output before simulating shutdown + * @param $expectNLAppending bool: Whether or not shutdown simulation is expected + * to add a newline to the output. If false, $preShutdownOutput is the + * expected output after shutdown simulation. Otherwise, + * $preShutdownOutput with an appended newline is the expected output + * after shutdown simulation. + */ + private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->m->simulateShutdown(); + $this->m = null; + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + + + // Although the following tests do not seem to be too consistent (compare for + // example the newlines within the test.*StringString tests, or the + // test.*Intermittent.* tests), the objective of these tests is not to describe + // consistent behavior, but rather currently existing behavior. + + + function testOutputEmpty() { + $this->m->output( "" ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputString() { + $this->m->output( "foo" ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputStringString() { + $this->m->output( "foo" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputStringNL() { + $this->m->output( "foo\n" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputStringNLNL() { + $this->m->output( "foo\n\n" ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputStringNLString() { + $this->m->output( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputStringNLStringNL() { + $this->m->output( "foo\nbar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLLinewise() { + $this->m->output( "foo\n" ); + $this->m->output( "bar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitrary() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\n" ); + $this->m->output( "ba" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitraryAgain() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\nb" ); + $this->m->output( "a" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelEmpty() { + $this->m->output( "", null ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputWNullChannelString() { + $this->m->output( "foo", null ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputWNullChannelStringString() { + $this->m->output( "foo", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelStringNL() { + $this->m->output( "foo\n", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputWNullChannelStringNLNL() { + $this->m->output( "foo\n\n", null ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputWNullChannelStringNLString() { + $this->m->output( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputWNullChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", null ); + $this->m->output( "bar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitrary() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\n", null ); + $this->m->output( "ba", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\nb", null ); + $this->m->output( "a", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWChannelString() { + $this->m->output( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNL() { + $this->m->output( "foo\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNLNL() { + // If this test fails, note that output takes strings with double line + // endings (although output's implementation in this situation calls + // outputChanneled with a string ending in a nl ... which is not allowed + // according to the documentation of outputChanneled) + $this->m->output( "foo\n\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\n", true ); + } + + function testOutputWChannelStringNLString() { + $this->m->output( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitrary() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\n", "bazChannel" ); + $this->m->output( "ba", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\nb", "bazChannel" ); + $this->m->output( "a", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWMultipleChannelsChannelChange() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWMultipleChannelsChannelChangeNL() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->m->output( "qux\n", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWAndWOChannelStringStartWO() { + $this->m->output( "foo" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux" ); + $this->m->output( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nquxquux", true ); + } + + function testOutputWAndWOChannelStringStartW() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar" ); + $this->m->output( "qux", "bazChannel" ); + $this->m->output( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", false ); + } + + function testOutputWChannelTypeSwitch() { + $this->m->output( "foo", 1 ); + $this->m->output( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputIntermittentEmpty() { + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalse() { + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalseAfterOtherChannel() { + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "qux\nfoobar", false ); + } + + function testOutputWNullChannelIntermittentEmpty() { + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelIntermittentFalse() { + $this->m->output( "foo", null ); + $this->m->output( false, null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWChannelIntermittentEmpty() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelIntermittentFalse() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( false, "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + // Note that (per documentation) outputChanneled does take strings that end + // in \n, hence we do not test such strings. + + function testOutputChanneledEmpty() { + $this->m->outputChanneled( "" ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledString() { + $this->m->outputChanneled( "foo" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledStringString() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLString() { + $this->m->outputChanneled( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "\nb" ); + $this->m->outputChanneled( "a" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "r" ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWNullChannelEmpty() { + $this->m->outputChanneled( "", null ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledWNullChannelString() { + $this->m->outputChanneled( "foo", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledWNullChannelStringString() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "\nb", null ); + $this->m->outputChanneled( "a", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "r", null ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWChannelString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputChanneledWChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWChannelStringString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "\nb", "bazChannel" ); + $this->m->outputChanneled( "a", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "r", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux", "quuxChannel" ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelAfterNullChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "quuxChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWAndWOChannelStringStartWO() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux" ); + $this->m->outputChanneled( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", true ); + } + + function testOutputChanneledWAndWOChannelStringStartW() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar" ); + $this->m->outputChanneled( "qux", "bazChannel" ); + $this->m->outputChanneled( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", false ); + } + + function testOutputChanneledWChannelTypeSwitch() { + $this->m->outputChanneled( "foo", 1 ); + $this->m->outputChanneled( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWOChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWOChannelIntermittentFalse() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( false ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( false, null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( false, "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testCleanupChanneledClean() { + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testCleanupChanneledAfterOutput() { + $this->m->output( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWNullChannel() { + $this->m->output( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWChannel() { + $this->m->output( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutput() { + $this->m->output( "foo\n" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWNullChannel() { + $this->m->output( "foo\n", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWChannel() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWOChannel() { + $this->m->outputChanneled( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWNullChannel() { + $this->m->outputChanneled( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWChannel() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutput() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo" ); + $m2->output( "bar" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", null ); + $m2->output( "bar", null ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", "bazChannel" ); + $m2->output( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", null ); + $m2->output( "bar\n", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", "bazChannel" ); + $m2->output( "bar\n", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneled() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo" ); + $m2->outputChanneled( "bar" ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", null ); + $m2->outputChanneled( "bar", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before first cleanup" ); + $this->m->cleanupChanneled(); + $this->assertEquals( "foobar\n", $this->getActualOutput(), + "Output after first cleanup" ); + $m2->cleanupChanneled(); + $this->assertEquals( "foobar\n\n", $this->getActualOutput(), + "Output after second cleanup" ); + + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n\n", false ); + } + + +} diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..cc00e6e5 --- /dev/null +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,278 @@ +dump !== null ) { + $this->dump->close(); + } + + // Bug 37458, parent teardown need to be done after closing the + // dump or it might cause some permissions errors. + parent::tearDown(); + } + + /** + * asserts that a prefetch yields an expected string + * + * @param $expected string|null: the exepcted result of the prefetch + * @param $page int: the page number to prefetch the text for + * @param $revision int: the revision number to prefetch the text for + */ + private function assertPrefetchEquals( $expected, $page, $revision ) { + $this->assertEquals( $expected, $this->dump->prefetch( $page, $revision ), + "Prefetch of page $page revision $revision" ); + + } + + function testSequential() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeRevisionMissToRevision() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 3 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + } + + function testSynchronizeRevisionMissToPage() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizePageMiss() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 3, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testPageMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 6, 40 ); + } + + function testRevisionMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 4, 40 ); + } + + function testSynchronizePageMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 0, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSynchronizeRevisionMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 1, -2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSequentialAcrossFiles() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2, 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeSkipAcrossFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $fname3 = $this->setUpPrefetch( array( 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 . ";" . $fname3 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeMissInWholeFirstFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + + /** + * Constructs a temporary file that can be used for prefetching + * + * The temporary file is removed by DumpBackup upon tearDown. + * + * @param $requested_pages Array The indices of the page parts that should + * go into the prefetch file. 1,2,4 are available. + * @return String The file name of the created temporary file + */ + private function setUpPrefetch( $requested_pages = array( 1, 2, 4 ) ) { + // The file name, where we store the prepared prefetch file + $fname = $this->getNewTempFile(); + + // The header of every prefetch file + $header = ' + +'; + + // Putting together the content of the prefetch files + $content = $header; + foreach ( $requested_pages as $i ) { + $this->assertTrue( array_key_exists( $i, $available_pages ), + "Check for availability of requested page " . $i ); + $content .= $available_pages[$i]; + } + $content .= $tail; + + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared prefetch" ); + + return $fname; + } + +} diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php new file mode 100644 index 00000000..09623445 --- /dev/null +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,584 @@ +tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + $ns = $this->getDefaultWikitextNS(); + + try { + // Simple page + $title = Title::newFromText( 'BackupDumperTestP1', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + // Page with more than one revision + $title = Title::newFromText( 'BackupDumperTestP2', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + // Deleted page. + $title = Title::newFromText( 'BackupDumperTestP3', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + // Page from non-default namespace + + if ( $ns === NS_TALK ) { + //@todo: work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK ); + $page = WikiPage::factory( $title ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + + } + + function testPlain() { + // Setting up the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub, + "--output=file:" . $nameFull ) ); + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testPrefetchPlain() { + // The mapping between ids and text, for the hits of the prefetch mock + $prefetchMap = array( + array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ), + array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ) + ); + + // The mock itself + $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', false ); + $prefetchMock->expects( $this->exactly( 6 ) ) + ->method( 'prefetch' ) + ->will( $this->returnValueMap( $prefetchMap ) ); + + // Setting up of the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array( "--stub=file:" + . $nameStub, "--output=file:" . $nameFull ) ); + $dumper->prefetch = $prefetchMock; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "Prefetch_________1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "Prefetch_________2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + } + + /** + * Ensures that checkpoint dumps are used and written, by successively increasing the + * stub size and dumping until the duration crosses a threshold. + * + * @param $checkpointFormat string: Either "file" for plain text or "gzip" for gzipped + * checkpoint files. + */ + private function checkpointHelper( $checkpointFormat = "file" ) { + // Getting temporary names + $nameStub = $this->getNewTempFile(); + $nameOutputDir = $this->getNewTempDirectory(); + + $stderr = fopen( 'php://output', 'a' ); + if ( $stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + $iterations = 32; // We'll start with that many iterations of revisions in stub + $lastDuration = 0; + $minDuration = 2; // We want the dump to take at least this many seconds + $checkpointAfter = 0.5; // Generate checkpoint after this many seconds + + + // Until a dump takes at least $minDuration seconds, perform a dump and check + // duration. If the dump did not take long enough increase the iteration + // count, to generate a bigger stub file next time. + while ( $lastDuration < $minDuration ) { + + // Setting up the dump + wfRecursiveRemoveDir( $nameOutputDir ); + $this->assertTrue( wfMkdirParents( $nameOutputDir ), + "Creating temporary output directory " ); + $this->setUpStub( $nameStub, $iterations ); + $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub, + "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full", + "--maxtime=1" /*This is in minutes. Fixup is below*/, + "--checkpointfile=checkpoint-%s-%s.xml.gz" ) ); + $dumper->setDb( $this->db ); + $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute + $dumper->stderr = $stderr; + + // The actual dump and taking time + $ts_before = microtime( true ); + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + $ts_after = microtime( true ); + $lastDuration = $ts_after - $ts_before; + + // Handling increasing the iteration count for the stubs + if ( $lastDuration < $minDuration ) { + $old_iterations = $iterations; + if ( $lastDuration > 0.2 ) { + // lastDuration is big enough, to allow an educated guess + $factor = ( $minDuration + 0.5 ) / $lastDuration; + if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) { + // educated guess is reasonable + $iterations = (int)( $iterations * $factor ); + } + } + + if ( $old_iterations == $iterations ) { + // Heuristics were not applied, so we just *2. + $iterations *= 2; + } + + $this->assertLessThan( 50000, $iterations, + "Emergency stop against infinitely increasing iteration " + . "count ( last duration: $lastDuration )" ); + } + } + + // The dump (hopefully) did take long enough to produce more than one + // checkpoint file. + // + // We now check all the checkpoint files for validity. + + $files = scandir( $nameOutputDir ); + $this->assertTrue( asort( $files ), "Sorting files in temporary directory" ); + $fileOpened = false; + $lookingForPage = 1; + $checkpointFiles = 0; + + // Each run of the following loop body tries to handle exactly 1 /page/ (not + // iteration of stub content). $i is only increased after having treated page 4. + for ( $i = 0; $i < $iterations; ) { + + // 1. Assuring a file is opened and ready. Skipping across header if + // necessary. + if ( !$fileOpened ) { + $this->assertNotEmpty( $files, "No more existing dump files, " + . "but not yet all pages found" ); + $fname = array_shift( $files ); + while ( $fname == "." || $fname == ".." ) { + $this->assertNotEmpty( $files, "No more existing dump" + . " files, but not yet all pages found" ); + $fname = array_shift( $files ); + } + if ( $checkpointFormat == "gzip" ) { + $this->gunzip( $nameOutputDir . "/" . $fname ); + } + $this->assertDumpStart( $nameOutputDir . "/" . $fname ); + $fileOpened = true; + $checkpointFiles++; + } + + // 2. Performing a single page check + switch ( $lookingForPage ) { + case 1: + // Page 1 + $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 2; + break; + + case 2: + // Page 2 + $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", + $this->revId2_3 + $i * self::$numOfRevs ); + $this->assertPageEnd(); + + $lookingForPage = 4; + break; + + case 4: + // Page 4 + $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK, + "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 1; + + // We dealt with the whole iteration. + $i++; + break; + + default: + $this->fail( "Bad setting for lookingForPage ($lookingForPage)" ); + } + + // 3. Checking for the end of the current checkpoint file + if ( $this->xml->nodeType == XMLReader::END_ELEMENT + && $this->xml->name == "mediawiki" + ) { + $this->assertDumpEnd(); + $fileOpened = false; + } + } + + // Assuring we completely read all files ... + $this->assertFalse( $fileOpened, "Currently read file still open?" ); + $this->assertEmpty( $files, "Remaining unchecked files" ); + + // ... and have dealt with more than one checkpoint file + $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" ); + + $this->expectETAOutput(); + } + + /** + * @group large + */ + function testCheckpointPlain() { + $this->checkpointHelper(); + } + + /** + * tests for working checkpoint generation in gzip format work. + * + * We keep this test in addition to the simpler self::testCheckpointPlain, as there + * were once problems when the used sinks were DumpPipeOutputs. + * + * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires + * PHP extensions, we go for gzip instead, which triggers the same relevant code + * paths while still being testable on more systems. + * + * @group large + */ + function testCheckpointGzip() { + $this->checkHasGzip(); + $this->checkpointHelper( "gzip" ); + } + + + /** + * Creates a stub file that is used for testing the text pass of dumps + * + * @param $fname string: (Optional) Absolute name of the file to write + * the stub into. If this parameter is null, a new temporary + * file is generated that is automatically removed upon + * tearDown. + * @param $iterations integer: (Optional) specifies how often the block + * of 3 pages should go into the stub file. The page and + * revision id increase further and further, while the text + * id of the first iteration is reused. The pages and revision + * of iteration > 1 have no corresponding representation in the + * database. + * @return string absolute filename of the stub + */ + private function setUpStub( $fname = null, $iterations = 1 ) { + if ( $fname === null ) { + $fname = $this->getNewTempFile(); + } + $header = '+ +'; + + + // An array holding the pages that are available for prefetch + $available_pages = array(); + + // Simple plain page + $available_pages[1] = 'wikisvn +http://localhost/wiki-svn/index.php/Main_Page + MediaWiki 1.21alpha +first-letter ++ +Media +Special ++ Talk +User +User talk +Wikisvn +Wikisvn talk +File +File talk +MediaWiki +MediaWiki talk +Template +Template talk +Help +Help talk +Category +Category talk ++ +'; + // Page with more than one revisions. Hole in rev ids. + $available_pages[2] = 'BackupDumperTestP1 +0 +1 ++ +1 +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP1Summary1 +0bolhl6ol7i6x0e7yq91gxgaan39j87 +BackupDumperTestP1Text1 +1 +1 ++ +'; + // Page with id higher than previous id + 1 + $available_pages[4] = 'BackupDumperTestP2 +0 +2 ++ +2 +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary1 +jprywrymfhysqllua29tj3sc7z39dl2 +BackupDumperTestP2Text1 +1 +1 ++ +5 +2 +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary4 extra +6o1ciaxa6pybnqprmungwofc4lv00wv +BackupDumperTestP2Text4 some additional Text +1 +1 ++ +'; + + // The common ending for all files + $tail = 'Talk:BackupDumperTestP1 +1 +4 ++ +8 +2012-04-01T16:46:05Z ++ +127.0.0.1 +Talk BackupDumperTestP1 Summary1 +nktofwzd0tl192k3zfepmlzxoax1lpe +1 +1 +Talk about BackupDumperTestP1 Text1 ++ +'; + + $content = $header; + $iterations = intval( $iterations ); + for ( $i = 0; $i < $iterations; $i++ ) { + + $page1 = '+ +'; + $tail = 'wikisvn +http://localhost/wiki-svn/index.php/Main_Page + MediaWiki 1.21alpha +first-letter ++ +Media +Special ++ Talk +User +User talk +Wikisvn +Wikisvn talk +File +File talk +MediaWiki +MediaWiki talk +Template +Template talk +Help +Help talk +Category +Category talk ++ +'; + $page2 = 'BackupDumperTestP1 +0 +' . ( $this->pageId1 + $i * self::$numOfPages ) . ' ++ +' . ( $this->revId1_1 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP1Summary1 +0bolhl6ol7i6x0e7yq91gxgaan39j87 +wikitext +text/x-wiki ++ + +'; + // page 3 not in stub + + $page4 = 'BackupDumperTestP2 +0 +' . ( $this->pageId2 + $i * self::$numOfPages ) . ' ++ +' . ( $this->revId2_1 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary1 +jprywrymfhysqllua29tj3sc7z39dl2 +wikitext +text/x-wiki ++ + +' . ( $this->revId2_2 + $i * self::$numOfRevs ) . ' +' . ( $this->revId2_1 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary2 +b7vj5ks32po5m1z1t1br4o7scdwwy95 +wikitext +text/x-wiki ++ + +' . ( $this->revId2_3 + $i * self::$numOfRevs ) . ' +' . ( $this->revId2_2 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary3 +jfunqmh1ssfb8rs43r19w98k28gg56r +wikitext +text/x-wiki ++ + +' . ( $this->revId2_4 + $i * self::$numOfRevs ) . ' +' . ( $this->revId2_3 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +BackupDumperTestP2Summary4 extra +6o1ciaxa6pybnqprmungwofc4lv00wv +wikitext +text/x-wiki ++ + +'; + $content .= $page1 . $page2 . $page4; + } + $content .= $tail; + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared stub" ); + return $fname; + } +} diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php new file mode 100644 index 00000000..5cf172e6 --- /dev/null +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,230 @@ +setPerformer( $user ); + $logEntry->setTarget( Title::newFromText( $title, $ns ) ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + if ( $parameters !== null ) { + $logEntry->setParameters( $parameters ); + } + return $logEntry->insert(); + } + + function addDBData() { + $this->tablesUsed[] = 'logging'; + $this->tablesUsed[] = 'user'; + + try { + $user1 = User::newFromName( 'BackupDumperLogUserA' ); + $this->userId1 = $user1->getId(); + if ( $this->userId1 === 0 ) { + $user1->addToDatabase(); + $this->userId1 = $user1->getId(); + } + $this->assertGreaterThan( 0, $this->userId1 ); + + $user2 = User::newFromName( 'BackupDumperLogUserB' ); + $this->userId2 = $user2->getId(); + if ( $this->userId2 === 0 ) { + $user2->addToDatabase(); + $this->userId2 = $user2->getId(); + } + $this->assertGreaterThan( 0, $this->userId2 ); + + $this->logId1 = $this->addLogEntry( 'type', 'subtype', + $user1, NS_MAIN, "PageA" ); + $this->assertGreaterThan( 0, $this->logId1 ); + + $this->logId2 = $this->addLogEntry( 'supress', 'delete', + $user2, NS_TALK, "PageB", "SomeComment" ); + $this->assertGreaterThan( 0, $this->logId2 ); + + $this->logId3 = $this->addLogEntry( 'move', 'delete', + $user2, NS_MAIN, "PageA", "SomeOtherComment", + array( 'key1' => 1, 3 => 'value3' ) ); + $this->assertGreaterThan( 0, $this->logId3 ); + + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + + /** + * asserts that the xml reader is at the beginning of a log entry and skips over + * it while analyzing it. + * + * @param $id int: id of the log entry + * @param $user_name string: user name of the log entry's performer + * @param $user_id int: user id of the log entry 's performer + * @param $comment string|null: comment of the log entry. If null, the comment + * text is ignored. + * @param $type string: type of the log entry + * @param $subtype string: subtype of the log entry + * @param $title string: title of the log entry's target + * @param $parameters array: (optional) unserialized data accompanying the log entry + */ + private function assertLogItem( $id, $user_name, $user_id, $comment, $type, + $subtype, $title, $parameters = array() + ) { + + $this->assertNodeStart( "logitem" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "username", $user_name ); + $this->assertTextNode( "id", $user_id ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + if ( $comment !== null ) { + $this->assertTextNode( "comment", $comment ); + } + $this->assertTextNode( "type", $type ); + $this->assertTextNode( "action", $subtype ); + $this->assertTextNode( "logtitle", $title ); + + $this->assertNodeStart( "params" ); + $parameters_xml = unserialize( $this->xml->value ); + $this->assertEquals( $parameters, $parameters_xml ); + $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" ); + $this->assertNodeEnd( "params" ); + $this->skipWhitespace(); + + $this->assertNodeEnd( "logitem" ); + $this->skipWhitespace(); + } + + function testPlain() { + global $wgContLang; + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + // Analyzing the dumped data + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + } + + function testXmlDumpsBackupUseCaseLogging() { + global $wgContLang; + + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=gzip:" . $fname, + "--reporting=2" ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup demands reporting, although this is currently not + // implemented in BackupDumper, when dumping logging data. We + // nevertheless capture the output of the dump process already now, + // to be able to alert (once dumping produces reports) that this test + // needs updates. + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Analyzing the dumped data + $this->gunzip( $fname ); + + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + + // Currently, no reporting is implemented. Alert via failure, once + // this changes. + // If reporting for log dumps has been implemented, please update + // the following statement to catch good output + $this->expectOutputString( '' ); + } + +} diff --git a/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php new file mode 100644 index 00000000..07c76705 --- /dev/null +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,408 @@ +setMwGlobals( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ), + ) ); + + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + $this->namespace = $this->getDefaultWikitextNS(); + $this->talk_namespace = NS_TALK; + + if ( $this->namespace === $this->talk_namespace ) { + //@todo: work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $this->pageTitle1 = Title::newFromText( 'BackupDumperTestP1', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle1 ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + $this->pageTitle2 = Title::newFromText( 'BackupDumperTestP2', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle2 ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + $this->pageTitle3 = Title::newFromText( 'BackupDumperTestP3', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle3 ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + $this->pageTitle4 = Title::newFromText( 'BackupDumperTestP1', $this->talk_namespace ); + $page = WikiPage::factory( $this->pageTitle4 ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + + } + + function testFullTextPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testFullStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubGzip() { + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=gzip:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->gunzip( $fname ); + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + + function testXmlDumpsBackupUseCase() { + // xmldumps-backup typically performs a single dump that that writes + // out three files + // * gzipped stubs of everything (meta-history) + // * gzipped stubs of latest revisions of all pages (meta-current) + // * gzipped stubs of latest revisions of all pages of namespage 0 + // (articles) + // + // We reproduce such a setup with our mini fixture, although we omit + // chunks, and all the other gimmicks of xmldumps-backup. + // + $this->checkHasGzip(); + + $fnameMetaHistory = $this->getNewTempFile(); + $fnameMetaCurrent = $this->getNewTempFile(); + $fnameArticles = $this->getNewTempFile(); + + $dumper = new BackupDumper( array( "--output=gzip:" . $fnameMetaHistory, + "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", + "--output=gzip:" . $fnameArticles, "--filter=latest", + "--filter=notalk", "--filter=namespace:!NS_USER", + "--reporting=1000" ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup uses reporting. We will not check the exact reported + // message, as they are dependent on the processing power of the used + // computer. We only check that reporting does not crash the dumping + // and that something is reported + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Checking meta-history ------------------------------------------------- + + $this->gunzip( $fnameMetaHistory ); + $this->assertDumpStart( $fnameMetaHistory ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking meta-current ------------------------------------------------- + + $this->gunzip( $fnameMetaCurrent ); + $this->assertDumpStart( $fnameMetaCurrent ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking articles ------------------------------------------------- + + $this->gunzip( $fnameArticles ); + $this->assertDumpStart( $fnameArticles ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + // -> Page is not in $this->namespace. Hence not visible + + $this->assertDumpEnd(); + + $this->expectETAOutput(); + } + + +} diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php new file mode 100644 index 00000000..4d1d45d6 --- /dev/null +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,240 @@ + 0 ); + + + /** + * Data for the fake stdin + * + * @param $stdin String The string to be used instead of stdin + */ + function mockStdin( $stdin ) { + $this->mockStdinText = $stdin; + $this->mockSetUp = true; + } + + /** + * Gets invocation counters for mocked methods. + * + * @return Array An array, whose keys are function names. The corresponding values + * denote the number of times the function has been invoked. + */ + function mockGetInvocations() { + return $this->mockInvocations; + } + + // ----------------------------------------------------------------- + // Mocked functions from FetchText follow. + + function getStdin( $len = null ) { + $this->mockInvocations['getStdin']++; + if ( $len !== null ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin with non null parameter" ); + } + + if ( !$this->mockSetUp ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin before setting up rerouting" ); + } + + return fopen( 'data://text/plain,' . $this->mockStdinText, 'r' ); + } + +} + +/** + * TestCase for FetchText + * + * @group Database + * @group Dump + */ +class FetchTextTest extends MediaWikiTestCase { + + // We add 5 Revisions for this test. Their corresponding text id's + // are stored in the following 5 variables. + private $textId1; + private $textId2; + private $textId3; + private $textId4; + private $textId5; + + + /** + * @var Exception|null As the current MediaWikiTestCase::run is not + * robust enough to recover from thrown exceptions directly, we cannot + * throw frow within addDBData, although it would be appropriate. Hence, + * we catch the exception and store it until we are in setUp and may + * finally rethrow the exception without crashing the test suite. + */ + private $exceptionFromAddDBData; + + /** + * @var FetchText the (mocked) FetchText that is to test + */ + private $fetchText; + + /** + * Adds a revision to a page, while returning the resuting text's id + * + * @param $page WikiPage The page to add the revision to + * @param $text String The revisions text + * @param $text String The revisions summare + * + * @throws MWExcepion + */ + private function addRevision( $page, $text, $summary ) { + $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $id = $revision->getTextId(); + if ( $id > 0 ) { + return $id; + } + } + throw new MWException( "Could not determine text id" ); + } + + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + $wikitextNamespace = $this->getDefaultWikitextNS(); + + try { + $title = Title::newFromText( 'FetchTextTestPage1', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId1 = $this->addRevision( $page, "FetchTextTestPage1Text1", "FetchTextTestPage1Summary1" ); + + $title = Title::newFromText( 'FetchTextTestPage2', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId2 = $this->addRevision( $page, "FetchTextTestPage2Text1", "FetchTextTestPage2Summary1" ); + $this->textId3 = $this->addRevision( $page, "FetchTextTestPage2Text2", "FetchTextTestPage2Summary2" ); + $this->textId4 = $this->addRevision( $page, "FetchTextTestPage2Text3", "FetchTextTestPage2Summary3" ); + $this->textId5 = $this->addRevision( $page, "FetchTextTestPage2Text4 some additional Text ", "FetchTextTestPage2Summary4 extra " ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData + $this->exceptionFromAddDBData = $e; + } + } + + + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $this->fetchText = new SemiMockedFetchText(); + } + + + /** + * Helper to relate FetchText's input and output + */ + private function assertFilter( $input, $expectedOutput ) { + $this->fetchText->mockStdin( $input ); + $this->fetchText->execute(); + $invocations = $this->fetchText->mockGetInvocations(); + $this->assertEquals( 1, $invocations['getStdin'], + "getStdin invocation counter" ); + $this->expectOutputString( $expectedOutput ); + } + + + // Instead of the following functions, a data provider would be great. + // However, as data providers are evaluated /before/ addDBData, a data + // provider would not know the required ids. + + function testExistingSimple() { + $this->assertFilter( $this->textId2, + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSimpleWithNewline() { + $this->assertFilter( $this->textId2 . "\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSeveral() { + $this->assertFilter( "$this->textId1\n$this->textId5\n" + . "$this->textId3\n$this->textId3", + implode( "", array( + $this->textId1 . "\n23\nFetchTextTestPage1Text1", + $this->textId5 . "\n44\nFetchTextTestPage2Text4 " + . "some additional Text", + $this->textId3 . "\n23\nFetchTextTestPage2Text2", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + + function testEmpty() { + $this->assertFilter( "", null ); + } + + function testNonExisting() { + $this->assertFilter( $this->textId5 + 10, ( $this->textId5 + 10 ) . "\n-1\n" ); + } + + function testNegativeInteger() { + $this->assertFilter( "-42", "-42\n-1\n" ); + } + + function testFloatingPointNumberExisting() { + // float -> int -> revision + $this->assertFilter( $this->textId3 + 0.14159, + $this->textId3 . "\n23\nFetchTextTestPage2Text2" ); + } + + function testFloatingPointNumberNonExisting() { + $this->assertFilter( $this->textId5 + 3.14159, + ( $this->textId5 + 3 ) . "\n-1\n" ); + } + + function testCharacters() { + $this->assertFilter( "abc", "0\n-1\n" ); + } + + function testMix() { + $this->assertFilter( "ab\n" . $this->textId4 . ".5cd\n\nefg\n" . $this->textId2 + . "\n" . $this->textId3, + implode( "", array( + "0\n-1\n", + $this->textId4 . "\n23\nFetchTextTestPage2Text3", + "0\n-1\n", + "0\n-1\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + +} diff --git a/tests/phpunit/maintenance/getSlaveServerTest.php b/tests/phpunit/maintenance/getSlaveServerTest.php new file mode 100644 index 00000000..699571b7 --- /dev/null +++ b/tests/phpunit/maintenance/getSlaveServerTest.php @@ -0,0 +1,69 @@ +db->getType() === 'sqlite' ) { + // for SQLite, only the empty string is a good server name + return ''; + } + + $octet = '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; + $ip = "(($octet\.){3}$octet)"; + + $label = '([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)'; + $hostname = "($label(\.$label)*)"; + + return "($ip|$hostname)(:[0-9]{1,5})?"; + } + + function testPlain() { + $gss = new GetSlaveServer(); + $gss->execute(); + + $this->expectOutputRegex( "/^" . self::getServerRE() . "\n$/D" ); + } + + function testXmlDumpsBackupUseCase() { + global $wgDBprefix; + + global $argv; + $argv = array( null, "--globals" ); + + $gss = new GetSlaveServer(); + $gss->loadParamsAndArgs(); + $gss->execute(); + $gss->globals(); + + // The main answer + $output = $this->getActualOutput(); + $firstLineEndPos = strpos( $output, "\n" ); + if ( $firstLineEndPos === false ) { + $this->fail( "Could not find end of first line of output" ); + } + $firstLine = substr( $output, 0, $firstLineEndPos ); + $this->assertRegExp( "/^" . self::getServerRE() . "$/D", + $firstLine, "DB Server" ); + + // xmldumps-backup relies on the wgDBprefix in the output. + $this->expectOutputRegex( "/^[[:space:]]*\[wgDBprefix\][[:space:]]*=> " + . $wgDBprefix . "$/m" ); + } + + +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php new file mode 100644 index 00000000..2ec07440 --- /dev/null +++ b/tests/phpunit/phpunit.php @@ -0,0 +1,111 @@ +#!/usr/bin/env php +addOption( 'with-phpunitdir', + 'Directory to include PHPUnit from, for example when using a git fetchout from upstream. Path will be prepended to PHP `include_path`.', + false, # not required + true # need arg + ); + } + + public function finalSetup() { + parent::finalSetup(); + + global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; + global $wgLanguageConverterCacheType, $wgUseDatabaseMessages; + global $wgLocaltimezone, $wgLocalisationCacheConf; + global $wgDevelopmentWarnings; + + // wfWarn should cause tests to fail + $wgDevelopmentWarnings = true; + + $wgMainCacheType = CACHE_NONE; + $wgMessageCacheType = CACHE_NONE; + $wgParserCacheType = CACHE_NONE; + $wgLanguageConverterCacheType = CACHE_NONE; + + $wgUseDatabaseMessages = false; # Set for future resets + + // Assume UTC for testing purposes + $wgLocaltimezone = 'UTC'; + + $wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; + + // Bug 44192 Do not attempt to send a real e-mail + Hooks::clear( 'AlternateUserMailer' ); + Hooks::register( 'AlternateUserMailer', + function() { return false; } + ); + } + + public function execute() { + global $IP; + + # Make sure we have --configuration or PHPUnit might complain + if ( !in_array( '--configuration', $_SERVER['argv'] ) ) { + //Hack to eliminate the need to use the Makefile (which sucks ATM) + array_splice( $_SERVER['argv'], 1, 0, + array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) ); + } + + # --with-phpunitdir let us override the default PHPUnit version + if ( $phpunitDir = $this->getOption( 'with-phpunitdir' ) ) { + # Sanity checks + if ( !is_dir( $phpunitDir ) ) { + $this->error( "--with-phpunitdir should be set to an existing directory", 1 ); + } + if ( !is_readable( $phpunitDir . "/PHPUnit/Runner/Version.php" ) ) { + $this->error( "No usable PHPUnit installation in $phpunitDir.\nAborting.\n", 1 ); + } + + # Now prepends provided PHPUnit directory + $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" ); + set_include_path( $phpunitDir + . PATH_SEPARATOR . get_include_path() ); + + # Cleanup $args array so the option and its value do not + # pollute PHPUnit + $key = array_search( '--with-phpunitdir', $_SERVER['argv'] ); + unset( $_SERVER['argv'][$key] ); // the option + unset( $_SERVER['argv'][$key + 1] ); // its value + $_SERVER['argv'] = array_values( $_SERVER['argv'] ); + + } + } + + public function getDbType() { + return Maintenance::DB_ADMIN; + } +} + +$maintClass = 'PHPUnitMaintClass'; +require( RUN_MAINTENANCE_IF_MAIN ); + +require_once( 'PHPUnit/Runner/Version.php' ); + +if ( PHPUnit_Runner_Version::id() !== '@package_version@' + && version_compare( PHPUnit_Runner_Version::id(), '3.6.7', '<' ) +) { + die( 'PHPUnit 3.6.7 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/resources/ResourcesTest.php b/tests/phpunit/resources/ResourcesTest.php new file mode 100644 index 00000000..71b8c676 --- /dev/null +++ b/tests/phpunit/resources/ResourcesTest.php @@ -0,0 +1,128 @@ +assertFileExists( $filename, + "File '$resource' referenced by '$module' must exist." + ); + } + + /** + * This ask the ResouceLoader for all registered files from modules + * created by ResourceLoaderFileModule (or one of its descendants). + * + * + * Since the raw data is stored in protected properties, we have to + * overrride this through ReflectionObject methods. + */ + public static function provideResourceFiles() { + global $wgEnableJavaScriptTest; + + // Test existance of test suite files as well + // (can't use setUp or setMwGlobals because providers are static) + $live_wgEnableJavaScriptTest = $wgEnableJavaScriptTest; + $wgEnableJavaScriptTest = true; + + // Array with arguments for the test function + $cases = array(); + + // Initialize ResourceLoader + $rl = new ResourceLoader(); + + // See also ResourceLoaderFileModule::__construct + $filePathProps = array( + // Lists of file paths + 'lists' => array( + 'scripts', + 'debugScripts', + 'loaderScripts', + 'styles', + ), + + // Collated lists of file paths + 'nested-lists' => array( + 'languageScripts', + 'skinScripts', + 'skinStyles', + ), + ); + + foreach ( $rl->getModuleNames() as $moduleName ) { + $module = $rl->getModule( $moduleName ); + if ( !$module instanceof ResourceLoaderFileModule ) { + continue; + } + + $reflectedModule = new ReflectionObject( $module ); + + $files = array(); + + foreach ( $filePathProps['lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $list = $property->getValue( $module ); + foreach ( $list as $key => $value ) { + // 'scripts' are numeral arrays. + // 'styles' can be numeral or associative. + // In case of associative the key is the file path + // and the value is the 'media' attribute. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + + foreach ( $filePathProps['nested-lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $lists = $property->getValue( $module ); + foreach ( $lists as $group => $list ) { + foreach ( $list as $key => $value ) { + // We need the same filter as for 'lists', + // due to 'skinStyles'. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + } + + // Get method for resolving the paths to full paths + $method = $reflectedModule->getMethod( 'getLocalPath' ); + $method->setAccessible( true ); + + // Populate cases + foreach ( $files as $file ) { + $cases[] = array( + $method->invoke( $module, $file ), + $module->getName(), + $file, + ); + } + + } + + // Restore settings + $wgEnableJavaScriptTest = $live_wgEnableJavaScriptTest; + + return $cases; + } + +} 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..3902b686 --- /dev/null +++ b/tests/phpunit/skins/SideBarTest.php @@ -0,0 +1,205 @@ +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(); + } + } + + protected function setUp() { + parent::setUp(); + $this->initMessagesHref(); + $this->skin = new SkinTemplate(); + $this->skin->getContext()->setLanguage( Language::factory( 'en' ) ); + } + + protected 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/ +' + ); + + } + + /** + * bug 33321 - Make sure there's a | after transforming. + * @group Database + */ + function testTrickyPipe() { + $this->assertSidebar( + array( 'Title' => array( + # The first 2 are skipped + # Doesn't really test the url properly + # because it will vary with $wgArticlePath et al. + # ** Baz|Fred + array( + 'text' => 'Fred', + 'href' => Title::newFromText( 'Baz' )->getLocalUrl(), + 'id' => 'n-Fred', + 'active' => null, + ), + array( + 'text' => 'title-to-display', + 'href' => Title::newFromText( 'page-to-go-to' )->getLocalUrl(), + 'id' => 'n-title-to-display', + 'active' => null, + ), + ) ), + '* Title +** {{PAGENAME|Foo}} +** Bar +** Baz|Fred +** {{PLURAL:1|page-to-go-to{{int:pipe-separator/en}}title-to-display|branch not taken}} +' + ); + } + + + #### 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..8f7e977f --- /dev/null +++ b/tests/phpunit/suite.xml @@ -0,0 +1,50 @@ + + + +Talk:BackupDumperTestP1 +1 +' . ( $this->pageId4 + $i * self::$numOfPages ) . ' ++ +' . ( $this->revId4_1 + $i * self::$numOfRevs ) . ' +2012-04-01T16:46:05Z ++ +127.0.0.1 +Talk BackupDumperTestP1 Summary1 +nktofwzd0tl192k3zfepmlzxoax1lpe +wikitext +text/x-wiki ++ + diff --git a/tests/phpunit/suites/ExtensionsTestSuite.php b/tests/phpunit/suites/ExtensionsTestSuite.php new file mode 100644 index 00000000..eec773db --- /dev/null +++ b/tests/phpunit/suites/ExtensionsTestSuite.php @@ -0,0 +1,33 @@ +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..28d38ab4 --- /dev/null +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -0,0 +1,206 @@ + 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => new FSFileBackend( array( + 'name' => 'local-backend', + 'lockManager' => 'fsLockManager', + 'containerPaths' => array( + 'local-public' => wfTempDir() . '/test-repo/public', + 'local-thumb' => wfTempDir() . '/test-repo/thumb', + 'local-temp' => wfTempDir() . '/test-repo/temp', + 'local-deleted' => wfTempDir() . '/test-repo/delete', + ) + ) ), + ); + foreach ( $tmpGlobals as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + $GLOBALS[$var] = $val; + } + + $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + + $wgEnableParserCache = false; + DeferredUpdates::clearPendingUpdates(); + $wgMemc = wfGetMainCache(); + $messageMemc = wfGetMessageCacheStorage(); + $parserMemc = wfGetParserCacheStorage(); + + // $wgContLang = new StubContLang; + $wgUser = new User; + $context = new RequestContext; + $wgLang = $context->getLanguage(); + $wgOut = $context->getOutput(); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgRequest = $context->getRequest(); + + if ( $wgStyleDirectory === false ) { + $wgStyleDirectory = "$IP/skins"; + } + + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + } + + protected function tearDown() { + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + // Restore backends + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + $this->teardownUploadDir( $this->uploadDir ); + + parent::tearDown(); + } + + 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', null, __METHOD__ ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); + + wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); + 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; + } +} -- cgit v1.2.2+ ++ +includes ++ +languages ++ + +skins ++ +maintenance ++ +AutoLoaderTest.php +StructureTest.php ++ +suites/UploadFromUrlTestSuite.php ++ +suites/ExtensionsTestSuite.php ++ ++ +Utility +Broken +ParserFuzz +Stub +