diff options
Diffstat (limited to 'tests/phpunit')
103 files changed, 9969 insertions, 555 deletions
diff --git a/tests/phpunit/MediaWikiLangTestCase.php b/tests/phpunit/MediaWikiLangTestCase.php index 783f0315..6dd8ea35 100644 --- a/tests/phpunit/MediaWikiLangTestCase.php +++ b/tests/phpunit/MediaWikiLangTestCase.php @@ -10,6 +10,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { public function setUp() { global $wgLanguageCode, $wgLang, $wgContLang; + parent::setUp(); + self::$oldLang = $wgLang; self::$oldContLang = $wgContLang; @@ -23,6 +25,7 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); MessageCache::singleton()->disable(); + } public function tearDown() { @@ -32,6 +35,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase { $wgContLang = self::$oldContLang; $wgLanguageCode = $wgContLang->getCode(); self::$oldContLang = self::$oldLang = null; + + parent::tearDown(); } } diff --git a/tests/phpunit/MediaWikiPHPUnitCommand.php b/tests/phpunit/MediaWikiPHPUnitCommand.php index ea385ad9..fca32515 100644 --- a/tests/phpunit/MediaWikiPHPUnitCommand.php +++ b/tests/phpunit/MediaWikiPHPUnitCommand.php @@ -37,7 +37,7 @@ class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command { # PHPUnit uses stream_resolve_include_path() internally # See bug 32022 set_include_path( - dirname( __FILE__ ) + __DIR__ .PATH_SEPARATOR . get_include_path() ); diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 6ec8bdc7..1cc45e08 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -6,6 +6,11 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { public $runDisabled = false; /** + * @var Array of TestUser + */ + public static $users; + + /** * @var DatabaseBase */ protected $db; @@ -17,6 +22,15 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { private static $dbSetup = false; /** + * Holds the paths of temporary files/directories created through getNewTempFile, + * and getNewTempDirectory + * + * @var array + */ + private $tmpfiles = array(); + + + /** * Table name prefixes. Oracle likes it shorter. */ const DB_PREFIX = 'unittest_'; @@ -71,13 +85,77 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } + /** + * obtains a new temporary file name + * + * The obtained filename is enlisted to be removed upon tearDown + * + * @returns string: absolute name of the temporary file + */ + protected function getNewTempFile() { + $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); + $this->tmpfiles[] = $fname; + return $fname; + } + + /** + * obtains a new temporary directory + * + * The obtained directory is enlisted to be removed (recursively with all its contained + * files) upon tearDown. + * + * @returns string: absolute name of the temporary directory + */ + protected function getNewTempDirectory() { + // Starting of with a temporary /file/. + $fname = $this->getNewTempFile(); + + // Converting the temporary /file/ to a /directory/ + // + // The following is not atomic, but at least we now have a single place, + // where temporary directory creation is bundled and can be improved + unlink( $fname ); + $this->assertTrue( wfMkdirParents( $fname ) ); + return $fname; + } + + protected function tearDown() { + // Cleaning up temporary files + foreach ( $this->tmpfiles as $fname ) { + if ( is_file( $fname ) || ( is_link( $fname ) ) ) { + unlink( $fname ); + } elseif ( is_dir( $fname ) ) { + wfRecursiveRemoveDir( $fname ); + } + } + + // clean up open transactions + if( $this->needsDB() && $this->db ) { + while( $this->db->trxLevel() > 0 ) { + $this->db->rollback(); + } + } + + parent::tearDown(); + } + 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 ); - return strpos( $rc->getDocComment(), '@group Database' ) !== false; + if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { + return true; + } + + return false; } /** @@ -260,10 +338,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } - public static function disableInterwikis( $prefix, &$data ) { - return false; - } - /** * Don't throw a warning if $function is deprecated and called later * @@ -275,4 +349,199 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { 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 ( is_object( $actual ) ) { + $this->assertInstanceOf( $type, $actual, $message ); + } + else { + $this->assertInternalType( $type, $actual, $message ); + } + } + } diff --git a/tests/phpunit/StructureTest.php b/tests/phpunit/StructureTest.php index f967c18d..17ea06c4 100644 --- a/tests/phpunit/StructureTest.php +++ b/tests/phpunit/StructureTest.php @@ -20,6 +20,7 @@ class StructureTest extends MediaWikiTestCase { 'MediaWikiLangTestCase', 'MediaWikiTestCase', 'PHPUnit_Framework_TestCase', + 'DumpTestCase', ) ); $testClassRegex = "^class .* extends ($testClassRegex)"; $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" . @@ -39,11 +40,14 @@ class StructureTest extends MediaWikiTestCase { $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 names must end with Test.' + "Unit test file in $rootPath must end with Test." ); } diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index b023fdcf..933767e7 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -11,15 +11,15 @@ if ( !defined( 'MW_PHPUNIT_TEST' ) ) { You are running these tests directly from phpunit. You may not have all globals correctly set. Running phpunit.php instead is recommended. EOF; - require_once ( dirname( __FILE__ ) . "/phpunit.php" ); + require_once ( __DIR__ . "/phpunit.php" ); } // Output a notice when running with older versions of PHPUnit -if ( !version_compare( PHPUnit_Runner_Version::id(), "3.4.1", ">" ) ) { +if ( version_compare( PHPUnit_Runner_Version::id(), "3.6.7", "<" ) ) { echo <<<EOF ******************************************************************************** -These tests run best with version PHPUnit 3.4.2 or better. Earlier versions may +These tests run best with version PHPUnit 3.6.7 or better. Earlier versions may show failures because earlier versions of PHPUnit do not properly implement dependencies. diff --git a/tests/phpunit/data/media/exif-gps.jpg b/tests/phpunit/data/media/exif-gps.jpg Binary files differindex f99b484d..40137340 100644 --- a/tests/phpunit/data/media/exif-gps.jpg +++ b/tests/phpunit/data/media/exif-gps.jpg diff --git a/tests/phpunit/data/xmp/gps.result.php b/tests/phpunit/data/xmp/gps.result.php new file mode 100644 index 00000000..2d1243d5 --- /dev/null +++ b/tests/phpunit/data/xmp/gps.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'GPSAltitude' => -3.14159265301, + 'GPSDOP' => '5/1', + 'GPSLatitude' => 88.51805555, + 'GPSLongitude' => -21.12356945, + 'GPSVersionID' => '2.2.0.0' + ) +); + diff --git a/tests/phpunit/data/xmp/gps.xmp b/tests/phpunit/data/xmp/gps.xmp new file mode 100644 index 00000000..e52d2c8a --- /dev/null +++ b/tests/phpunit/data/xmp/gps.xmp @@ -0,0 +1,17 @@ +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:exif='http://ns.adobe.com/exif/1.0/'> + <exif:GPSAltitude>103993/33102</exif:GPSAltitude> + <exif:GPSAltitudeRef>1</exif:GPSAltitudeRef> + <exif:GPSDOP>5/1</exif:GPSDOP> + <exif:GPSLatitude>88,31.083333N</exif:GPSLatitude> + <exif:GPSLongitude>21,7.414167W</exif:GPSLongitude> + <exif:GPSVersionID>2.2.0.0</exif:GPSVersionID> + </rdf:Description> + +</rdf:RDF> +</x:xmpmeta> +<?xpacket end='w'?> diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php new file mode 100644 index 00000000..ce65d494 --- /dev/null +++ b/tests/phpunit/docs/ExportDemoTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Test for the demo xml + * + * @group Dump + */ +class ExportDemoTest extends DumpTestCase { + + /** + * @group large + */ + function testExportDemo() { + $this->validateXmlFileAgainstXsd( "../../docs/export-demo.xml" ); + } + + /** + * Validates a xml file against the xsd. + * + * The validation is slow, because php has to read the xsd on each call. + * + * @param $fname string: name of file to validate + */ + protected function validateXmlFileAgainstXsd( $fname ) { + $version = WikiExporter::schemaVersion(); + + $dom = new DomDocument(); + $dom->load( $fname ); + + try { + $this->assertTrue( $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ), + "schemaValidate has found an error" ); + } catch( Exception $e ) { + $this->fail( "xml not valid against xsd: " . $e->getMessage() ); + } + } +} diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php index 02571b55..17cee6e8 100644 --- a/tests/phpunit/includes/ArticleTablesTest.php +++ b/tests/phpunit/includes/ArticleTablesTest.php @@ -17,14 +17,14 @@ class ArticleTablesTest extends MediaWikiLangTestCase { $wgLang = Language::factory( 'fr' ); $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0, false, $user ); - $templates1 = $page->getUsedTemplates(); + $templates1 = $title->getTemplateLinksFrom(); $wgLang = Language::factory( 'de' ); $page->mPreparedEdit = false; // In order to force the rerendering of the same wikitext // We need an edit, a purge is not enough to regenerate the tables $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE, false, $user ); - $templates2 = $page->getUsedTemplates(); + $templates2 = $title->getTemplateLinksFrom(); $this->assertEquals( $templates1, $templates2 ); $this->assertEquals( $templates1[0]->getFullText(), 'Historial' ); diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php index 749f40b4..0c95b8d1 100644 --- a/tests/phpunit/includes/BlockTest.php +++ b/tests/phpunit/includes/BlockTest.php @@ -67,7 +67,7 @@ class BlockTest extends MediaWikiLangTestCase { // $this->dumpBlocks(); $this->assertTrue( $this->block->equals( Block::newFromTarget('UTBlockee') ), "newFromTarget() returns the same block as the one that was made"); - + $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made"); } @@ -122,4 +122,109 @@ class BlockTest extends MediaWikiLangTestCase { array( false ) ); } + + function testBlockedUserCanNotCreateAccount() { + $username = 'BlockedUserToCreateAccountWith'; + $u = User::newFromName( $username ); + $u->setPassword( 'NotRandomPass' ); + $u->addToDatabase(); + unset( $u ); + + + // Sanity check + $this->assertNull( + Block::newFromTarget( $username ), + "$username should not be blocked" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertFalse( + $u->isBlockedFromCreateAccount(), + "Our sandbox user should be able to create account before being blocked" + ); + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ $username, + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + $block->insert(); + + // Reload block from DB + $userBlock = Block::newFromTarget( $username ); + $this->assertTrue( + (bool) $block->prevents( 'createaccount' ), + "Block object in DB should prevents 'createaccount'" + ); + + $this->assertInstanceOf( + 'Block', + $userBlock, + "'$username' block block object should be existent" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertTrue( + (bool) $u->isBlockedFromCreateAccount(), + "Our sandbox user '$username' should NOT be able to create account" + ); + } + + function testCrappyCrossWikiBlocks() { + // Delete the last round's block if it's still there + $oldBlock = Block::newFromTarget( 'UserOnForeignWiki' ); + if ( $oldBlock ) { + // An old block will prevent our new one from saving. + $oldBlock->delete(); + } + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ 'UserOnForeignWiki', + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + + $res = $block->insert( $this->db ); + $this->assertTrue( (bool)$res['id'], 'Block succeeded' ); + + // Local perspective (blockee on current wiki)... + $user = User::newFromName( 'UserOnForeignWiki' ); + $user->addToDatabase(); + // Set user ID to match the test value + $this->db->update( 'user', array( 'user_id' => 14146 ), array( 'user_id' => $user->getId() ) ); + $user = null; // clear + + $block = Block::newFromID( $res['id'] ); + $this->assertEquals( 'UserOnForeignWiki', $block->getTarget()->getName(), 'Correct blockee name' ); + $this->assertEquals( '14146', $block->getTarget()->getId(), 'Correct blockee id' ); + $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' ); + $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' ); + $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' ); + } } diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php index 6c3e6664..b5418dd7 100644 --- a/tests/phpunit/includes/CdbTest.php +++ b/tests/phpunit/includes/CdbTest.php @@ -8,7 +8,7 @@ class CdbTest extends MediaWikiTestCase { public function setUp() { if ( !CdbReader::haveExtension() ) { - $this->markTestIncomplete( 'This test requires native CDB support to be present.' ); + $this->markTestSkipped( 'Native CDB support is not available' ); } } diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php new file mode 100644 index 00000000..cdb6ed2f --- /dev/null +++ b/tests/phpunit/includes/DiffHistoryBlobTest.php @@ -0,0 +1,40 @@ +<?php + +class DiffHistoryBlobTest extends MediaWikiTestCase { + function setUp() { + if ( !extension_loaded( 'xdiff' ) ) { + $this->markTestSkipped( 'The xdiff extension is not available' ); + return; + } + if ( !function_exists( 'xdiff_string_rabdiff' ) ) { + $this->markTestSkipped( 'The version of xdiff extension is lower than 1.5.0' ); + return; + } + if ( !extension_loaded( 'hash' ) && !extension_loaded( 'mhash' ) ) { + $this->markTestSkipped( 'Neither the hash nor mhash extension is available' ); + return; + } + } + + /** + * Test for DiffHistoryBlob::xdiffAdler32() + * @dataProvider provideXdiffAdler32 + */ + function testXdiffAdler32( $input ) { + $xdiffHash = substr( xdiff_string_rabdiff( $input, '' ), 0, 4 ); + $dhb = new DiffHistoryBlob; + $myHash = $dhb->xdiffAdler32( $input ); + $this->assertSame( bin2hex( $xdiffHash ), bin2hex( $myHash ), + "Hash of " . addcslashes( $input, "\0..\37!@\@\177..\377" ) ); + } + + function provideXdiffAdler32() { + return array( + array( '', 'Empty string' ), + array( "\0", 'Null' ), + array( "\0\0\0", "Several nulls" ), + array( "Hello", "An ASCII string" ), + array( str_repeat( "x", 6000 ), "A string larger than xdiff's NMAX (5552)" ) + ); + } +} diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php index e98e9707..8ecfd7e5 100644 --- a/tests/phpunit/includes/EditPageTest.php +++ b/tests/phpunit/includes/EditPageTest.php @@ -1,5 +1,8 @@ <?php +/** + * @group Editing + */ class EditPageTest extends MediaWikiTestCase { /** @@ -27,7 +30,11 @@ class EditPageTest extends MediaWikiTestCase { array( "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo", "Section" - ) + ), + array( + "== Section== \t\r\n followed by whitespace (bug 35051)", + 'Section', + ), ); } } diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php index a9088cb2..903a6d25 100644 --- a/tests/phpunit/includes/ExtraParserTest.php +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -21,6 +21,8 @@ class ExtraParserTest extends MediaWikiTestCase { $this->options = new ParserOptions; $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) ); $this->parser = new Parser; + + MagicWord::clearCache(); } // Bug 8689 - Long numeric lines kill the parser @@ -146,7 +148,7 @@ class ExtraParserTest extends MediaWikiTestCase { */ function testTrackingCategory() { $title = Title::newFromText( __FUNCTION__ ); - $catName = wfMsgForContent( 'broken-file-category' ); + $catName = wfMessage( 'broken-file-category' )->inContentLanguage()->text(); $cat = Title::makeTitleSafe( NS_CATEGORY, $catName ); $expected = array( $cat->getDBkey() ); $parserOutput = $this->parser->parse( "[[file:nonexistent]]" , $title, $this->options ); diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php index 3cb42f12..9097d301 100644 --- a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -55,6 +55,12 @@ class GlobalTest extends MediaWikiTestCase { wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) ); } + function testExpandIRI() { + $this->assertEquals( + "https://te.wikibooks.org/wiki/ఉబుంటు_వాడుకరి_మార్గదర్శని", + wfExpandIRI( "https://te.wikibooks.org/wiki/%E0%B0%89%E0%B0%AC%E0%B1%81%E0%B0%82%E0%B0%9F%E0%B1%81_%E0%B0%B5%E0%B0%BE%E0%B0%A1%E0%B1%81%E0%B0%95%E0%B0%B0%E0%B0%BF_%E0%B0%AE%E0%B0%BE%E0%B0%B0%E0%B1%8D%E0%B0%97%E0%B0%A6%E0%B0%B0%E0%B1%8D%E0%B0%B6%E0%B0%A8%E0%B0%BF" ) ); + } + function testReadOnlyEmpty() { global $wgReadOnly; $wgReadOnly = null; @@ -305,7 +311,7 @@ class GlobalTest extends MediaWikiTestCase { function testDebugFunctionTest() { - global $wgDebugLogFile, $wgOut, $wgShowDebug, $wgDebugTimestamps; + global $wgDebugLogFile, $wgDebugTimestamps; $old_log_file = $wgDebugLogFile; $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' ); @@ -327,33 +333,7 @@ class GlobalTest extends MediaWikiTestCase { wfDebug( "\00305This has böth UTF and control chars\003" ); $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) ); unlink( $wgDebugLogFile ); - - - - $old_wgOut = $wgOut; - $old_wgShowDebug = $wgShowDebug; - - $wgOut = new MockOutputPage; - - $wgShowDebug = true; - - $message = "\00305This has böth UTF and control chars\003"; - - wfDebug( $message ); - - if( $wgOut->message == "JAJA is a stupid error message. Anyway, here's your message: $message" ) { - $this->assertTrue( true, 'MockOutputPage called, set the proper message.' ); - } - else { - $this->assertTrue( false, 'MockOutputPage was not called.' ); - } - - $wgOut = $old_wgOut; - $wgShowDebug = $old_wgShowDebug; - unlink( $wgDebugLogFile ); - - - + wfDebugMem(); $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); unlink( $wgDebugLogFile ); @@ -616,13 +596,3 @@ class GlobalTest extends MediaWikiTestCase { /* TODO: many more! */ } - -class MockOutputPage { - - public $message; - - function debug( $message ) { - $this->message = "JAJA is a stupid error message. Anyway, here's your message: $message"; - } -} - diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php new file mode 100644 index 00000000..4c4c4c04 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php @@ -0,0 +1,35 @@ +<?php + +class wfGetCaller extends MediaWikiTestCase { + + function testZero() { + $this->assertEquals( __METHOD__, wfGetCaller( 1 ) ); + } + + function callerOne() { + return wfGetCaller(); + } + + function testOne() { + $this->assertEquals( "wfGetCaller::testOne", self::callerOne() ); + } + + function intermediateFunction( $level = 2, $n = 0 ) { + if ( $n > 0 ) + return self::intermediateFunction( $level, $n - 1 ); + return wfGetCaller( $level ); + } + + function testTwo() { + $this->assertEquals( "wfGetCaller::testTwo", self::intermediateFunction() ); + } + + function testN() { + $this->assertEquals( "wfGetCaller::testN", self::intermediateFunction( 2, 0 ) ); + $this->assertEquals( "wfGetCaller::intermediateFunction", self::intermediateFunction( 1, 0 ) ); + + for ($i=0; $i < 10; $i++) + $this->assertEquals( "wfGetCaller::intermediateFunction", self::intermediateFunction( $i + 1, $i ) ); + } +} + diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php index 67b60d32..a18f7922 100644 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@ -6,15 +6,18 @@ class HtmlTest extends MediaWikiTestCase { private static $oldContLang; private static $oldLanguageCode; private static $oldNamespaces; + private static $oldHTML5; public function setUp() { - global $wgLang, $wgContLang, $wgLanguageCode; - + global $wgLang, $wgContLang, $wgLanguageCode, $wgHTML5; + + // Save globals self::$oldLang = $wgLang; self::$oldContLang = $wgContLang; self::$oldNamespaces = $wgContLang->getNamespaces(); self::$oldLanguageCode = $wgLanguageCode; - + self::$oldHTML5 = $wgHTML5; + $wgLanguageCode = 'en'; $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); @@ -36,18 +39,41 @@ class HtmlTest extends MediaWikiTestCase { 9 => 'MediaWiki_talk', 10 => 'Template', 11 => 'Template_talk', + 14 => 'Category', + 15 => 'Category_talk', 100 => 'Custom', 101 => 'Custom_talk', ) ); } - + public function tearDown() { - global $wgLang, $wgContLang, $wgLanguageCode; + global $wgLang, $wgContLang, $wgLanguageCode, $wgHTML5; + // Restore globals $wgContLang->setNamespaces( self::$oldNamespaces ); $wgLang = self::$oldLang; $wgContLang = self::$oldContLang; $wgLanguageCode = self::$oldLanguageCode; + $wgHTML5 = self::$oldHTML5; + } + + /** + * Wrapper to easily set $wgHTML5 = true. + * Original value will be restored after test completion. + * @todo Move to MediaWikiTestCase + */ + public function enableHTML5() { + global $wgHTML5; + $wgHTML5 = true; + } + /** + * Wrapper to easily set $wgHTML5 = false + * Original value will be restored after test completion. + * @todo Move to MediaWikiTestCase + */ + public function disableHTML5() { + global $wgHTML5; + $wgHTML5 = false; } public function testExpandAttributesSkipsNullAndFalse() { @@ -213,7 +239,7 @@ class HtmlTest extends MediaWikiTestCase { function testNamespaceSelector() { $this->assertEquals( - '<select id="namespace" name="namespace">' . "\n" . + '<select>' . "\n" . '<option value="0">(Main)</option>' . "\n" . '<option value="1">Talk</option>' . "\n" . '<option value="2">User</option>' . "\n" . @@ -226,12 +252,15 @@ class HtmlTest extends MediaWikiTestCase { '<option value="9">MediaWiki talk</option>' . "\n" . '<option value="10">Template</option>' . "\n" . '<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . '<option value="100">Custom</option>' . "\n" . '<option value="101">Custom talk</option>' . "\n" . '</select>', Html::namespaceSelector(), 'Basic namespace selector without custom options' ); + $this->assertEquals( '<label for="mw-test-namespace">Select a namespace:</label> ' . '<select id="mw-test-namespace" name="wpNamespace">' . "\n" . @@ -248,6 +277,8 @@ class HtmlTest extends MediaWikiTestCase { '<option value="9">MediaWiki talk</option>' . "\n" . '<option value="10">Template</option>' . "\n" . '<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . '<option value="100">Custom</option>' . "\n" . '<option value="101">Custom talk</option>' . "\n" . '</select>', @@ -257,77 +288,281 @@ class HtmlTest extends MediaWikiTestCase { ), 'Basic namespace selector with custom values' ); - } - function testNamespaceSelectorIdAndNameDefaultsAttributes() { - - $this->assertNsSelectorIdAndName( - 'namespace', 'namespace', - Html::namespaceSelector( array(), array( - # neither 'id' nor 'name' key given - )), - "Neither 'id' nor 'name' key given" + $this->assertEquals( + '<label>Select a namespace:</label> ' . +'<select>' . "\n" . +'<option value="0">(Main)</option>' . "\n" . +'<option value="1">Talk</option>' . "\n" . +'<option value="2">User</option>' . "\n" . +'<option value="3">User talk</option>' . "\n" . +'<option value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'<option value="100">Custom</option>' . "\n" . +'<option value="101">Custom talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( + array( 'label' => 'Select a namespace:' ) + ), + 'Basic namespace selector with a custom label but no id attribtue for the <select>' ); + } - $this->assertNsSelectorIdAndName( - 'namespace', 'select_name', - Html::namespaceSelector( array(), array( - 'name' => 'select_name', - # no 'id' key given - )), - "No 'id' key given, 'name' given" + function testCanFilterOutNamespaces() { + $this->assertEquals( +'<select>' . "\n" . +'<option value="2">User</option>' . "\n" . +'<option value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( + array( 'exclude' => array( 0, 1, 3, 100, 101 ) ) + ), + 'Namespace selector namespace filtering.' ); + } - $this->assertNsSelectorIdAndName( - 'select_id', 'namespace', - Html::namespaceSelector( array(), array( - 'id' => 'select_id', - # no 'name' key given - )), - "'id' given, no 'name' key given" + function testCanDisableANamespaces() { + $this->assertEquals( +'<select>' . "\n" . +'<option disabled="" value="0">(Main)</option>' . "\n" . +'<option disabled="" value="1">Talk</option>' . "\n" . +'<option disabled="" value="2">User</option>' . "\n" . +'<option disabled="" value="3">User talk</option>' . "\n" . +'<option disabled="" value="4">MyWiki</option>' . "\n" . +'<option value="5">MyWiki Talk</option>' . "\n" . +'<option value="6">File</option>' . "\n" . +'<option value="7">File talk</option>' . "\n" . +'<option value="8">MediaWiki</option>' . "\n" . +'<option value="9">MediaWiki talk</option>' . "\n" . +'<option value="10">Template</option>' . "\n" . +'<option value="11">Template talk</option>' . "\n" . +'<option value="14">Category</option>' . "\n" . +'<option value="15">Category talk</option>' . "\n" . +'<option value="100">Custom</option>' . "\n" . +'<option value="101">Custom talk</option>' . "\n" . +'</select>', + Html::namespaceSelector( array( + 'disable' => array( 0, 1, 2, 3, 4 ) + ) ), + 'Namespace selector namespace disabling' ); + } - $this->assertNsSelectorIdAndName( - 'select_id', 'select_name', - Html::namespaceSelector( array(), array( - 'id' => 'select_id', - 'name' => 'select_name', - )), - "Both 'id' and 'name' given" + /** + * @dataProvider providesHtml5InputTypes + */ + function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) { + $this->enableHTML5(); + $this->assertEquals( + '<input type="' . $HTML5InputType . '" />', + HTML::element( 'input', array( 'type' => $HTML5InputType ) ), + 'In HTML5, HTML::element() should accept type="' . $HTML5InputType . '"' ); } /** - * Helper to verify <select> attributes generated by Html::namespaceSelector() - * This helper expect the Html method to use 'namespace' as a default value for - * both 'id' and 'name' attributes. - * - * @param String $expectedId <select> id attribute value - * @param String $expectedName <select> name attribute value - * @param String $html Output of a call to Html::namespaceSelector() - * @param String $msg Optional message (default: '') - */ - function assertNsSelectorIdAndName( $expectedId, $expectedName, $html, $msg = '' ) { - $actualId = 'namespace'; - if( 1 === preg_match( '/id="(.+?)"/', $html, $m ) ) { - $actualId = $m[1]; + * List of input element types values introduced by HTML5 + * Full list at http://www.w3.org/TR/html-markup/input.html + */ + function providesHtml5InputTypes() { + $types = array( + 'datetime', + 'datetime-local', + 'date', + 'month', + 'time', + 'week', + 'number', + 'range', + 'email', + 'url', + 'search', + 'tel', + 'color', + ); + $cases = array(); + foreach( $types as $type ) { + $cases[] = array( $type ); } + return $cases; + } - $actualName = 'namespace'; - if( 1 === preg_match( '/name="(.+?)"/', $html, $m ) ) { - $actualName = $m[1]; - } - $this->assertEquals( - array( #expected - 'id' => $expectedId, - 'name' => $expectedName, - ), - array( #actual - 'id' => $actualId, - 'name' => $actualName, - ), - 'Html::namespaceSelector() got wrong id and/or name attribute(s). ' . $msg + /** + * Test out Html::element drops default value + * @cover Html::dropDefaults + * @dataProvider provideElementsWithAttributesHavingDefaultValues + */ + function testDropDefaults( $expected, $element, $message = '' ) { + $this->enableHTML5(); + $this->assertEquals( $expected, $element, $message ); + } + + function provideElementsWithAttributesHavingDefaultValues() { + # Use cases in a concise format: + # <expected>, <element name>, <array of attributes> [, <message>] + # Will be mapped to Html::element() + $cases = array(); + + ### Generic cases, match $attribDefault static array + $cases[] = array( '<area />', + 'area', array( 'shape' => 'rect' ) + ); + + $cases[] = array( '<button></button>', + 'button', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '<button></button>', + 'button', array( 'formenctype' => 'application/x-www-form-urlencoded' ) + ); + $cases[] = array( '<button></button>', + 'button', array( 'type' => 'submit' ) + ); + + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'height' => '150' ) + ); + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'width' => '300' ) + ); + # Also check with numeric values + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'height' => 150 ) + ); + $cases[] = array( '<canvas></canvas>', + 'canvas', array( 'width' => 300 ) + ); + + $cases[] = array( '<command />', + 'command', array( 'type' => 'command' ) + ); + + $cases[] = array( '<form></form>', + 'form', array( 'action' => 'GET' ) + ); + $cases[] = array( '<form></form>', + 'form', array( 'autocomplete' => 'on' ) + ); + $cases[] = array( '<form></form>', + 'form', array( 'enctype' => 'application/x-www-form-urlencoded' ) + ); + + $cases[] = array( '<input />', + 'input', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '<input />', + 'input', array( 'type' => 'text' ) + ); + + $cases[] = array( '<keygen />', + 'keygen', array( 'keytype' => 'rsa' ) ); + + $cases[] = array( '<link />', + 'link', array( 'media' => 'all' ) + ); + + $cases[] = array( '<menu></menu>', + 'menu', array( 'type' => 'list' ) + ); + + $cases[] = array( '<script></script>', + 'script', array( 'type' => 'text/javascript' ) + ); + + $cases[] = array( '<style></style>', + 'style', array( 'media' => 'all' ) + ); + $cases[] = array( '<style></style>', + 'style', array( 'type' => 'text/css' ) + ); + + $cases[] = array( '<textarea></textarea>', + 'textarea', array( 'wrap' => 'soft' ) + ); + + ### SPECIFIC CASES + + # <link type="text/css" /> + $cases[] = array( '<link />', + 'link', array( 'type' => 'text/css' ) + ); + + # <input /> specific handling + $cases[] = array( '<input type="checkbox" />', + 'input', array( 'type' => 'checkbox', 'value' => 'on' ), + 'Default value "on" is stripped of checkboxes', + ); + $cases[] = array( '<input type="radio" />', + 'input', array( 'type' => 'radio', 'value' => 'on' ), + 'Default value "on" is stripped of radio buttons', + ); + $cases[] = array( '<input type="submit" value="Submit" />', + 'input', array( 'type' => 'submit', 'value' => 'Submit' ), + 'Default value "Submit" is kept on submit buttons (for possible l10n issues)', + ); + $cases[] = array( '<input type="color" />', + 'input', array( 'type' => 'color', 'value' => '' ), + ); + $cases[] = array( '<input type="range" />', + 'input', array( 'type' => 'range', 'value' => '' ), + ); + + # <select /> specifc handling + $cases[] = array( '<select multiple=""></select>', + 'select', array( 'size' => '4', 'multiple' => true ), + ); + # .. with numeric value + $cases[] = array( '<select multiple=""></select>', + 'select', array( 'size' => 4, 'multiple' => true ), + ); + $cases[] = array( '<select></select>', + 'select', array( 'size' => '1', 'multiple' => false ), + ); + # .. with numeric value + $cases[] = array( '<select></select>', + 'select', array( 'size' => 1, 'multiple' => false ), + ); + + # Passing an array as value + $cases[] = array( '<a class="css-class-one css-class-two"></a>', + 'a', array( 'class' => array( 'css-class-one', 'css-class-two' ) ), + "dropDefaults accepts values given as an array" + ); + + # FIXME: doDropDefault should remove defaults given in an array + # Expected should be '<a></a>' + $cases[] = array( '<a class=""></a>', + 'a', array( 'class' => array( '', '' ) ), + "dropDefaults accepts values given as an array" + ); + + + # Craft the Html elements + $ret = array(); + foreach( $cases as $case ) { + $ret[] = array( + $case[0], + Html::element( $case[1], $case[2] ) + ); + } + return $ret; } } diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php index 4397b879..f50b2fe9 100644 --- a/tests/phpunit/includes/IPTest.php +++ b/tests/phpunit/includes/IPTest.php @@ -1,6 +1,7 @@ <?php /** * Tests for IP validity functions. Ported from /t/inc/IP.t by avar. + * @group IP */ class IPTest extends MediaWikiTestCase { @@ -14,9 +15,9 @@ class IPTest extends MediaWikiTestCase { $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' ); $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' ); $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' ); - $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' ); - $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' ); - $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurrence' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurrence, last at end' ); + $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurrence, firt at beginning' ); $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' ); $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' ); $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' ); @@ -505,4 +506,37 @@ class IPTest extends MediaWikiTestCase { array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ), ); } + + /** + * Test for IP::prettifyIP() + * @dataProvider provideIPsToPrettify + */ + function testPrettifyIP( $ip, $prettified ) { + $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" ); + } + + /** + * Provider for IP::testPrettifyIP() + */ + function provideIPsToPrettify() { + return array( + array( '0:0:0:0:0:0:0:0', '::' ), + array( '0:0:0::0:0:0', '::' ), + array( '0:0:0:1:0:0:0:0', '0:0:0:1::' ), + array( '0:0::f', '::f' ), + array( '0::0:0:0:33:fef:b', '::33:fef:b' ), + array( '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ), + array( '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ), + array( 'abbc:2004::0:0:0:0', 'abbc:2004::' ), + array( 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ), + array( '0:0:0:0:0:0:0:0/16', '::/16' ), + array( '0:0:0::0:0:0/64', '::/64' ), + array( '0:0::f/52', '::f/52' ), + array( '::0:0:33:fef:b/52', '::33:fef:b/52' ), + array( '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ), + array( '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ), + array( 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ), + array( 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ), + ); + } } diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php new file mode 100644 index 00000000..49462001 --- /dev/null +++ b/tests/phpunit/includes/LinksUpdateTest.php @@ -0,0 +1,154 @@ +<?php + +/** + * + * @group Database + * ^--- make sure temporary tables are used. + */ +class LinksUpdateTest extends MediaWikiTestCase { + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge ( $this->tablesUsed, + array( 'interwiki', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + function setUp() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'interwiki', + array('iw_prefix'), + array( 'iw_prefix' => 'linksupdatetest', + 'iw_url' => 'http://testing.com/wiki/$1', + 'iw_api' => 'http://testing.com/w/api.php', + 'iw_local' => 0, + 'iw_trans' => 0, + 'iw_wikiid' => 'linksupdatetest', + ) ); + } + + protected function makeTitleAndParserOutput( $name, $id ) { + $t = Title::newFromText( $name ); + $t->mArticleID = $id; # XXX: this is fugly + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + return array( $t, $po ); + } + + public function testUpdate_pagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLink( Title::newFromText( "Foo" ) ); + $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored + $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored + $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored + + $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Foo' ), + ) ); + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + $po->addLink( Title::newFromText( "Bar" ) ); + + $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Bar' ), + ) ); + } + + public function testUpdate_externallinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addExternalLink( "http://testing.com/wiki/Foo" ); + + $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array( + array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ), + ) ); + } + + public function testUpdate_categorylinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addCategory( "Foo", "FOO" ); + + $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array( + array( 'Foo', "FOO\nTESTING" ), + ) ); + } + + public function testUpdate_iwlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' ); + $po->addInterwikiLink( $target ); + + $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array( + array( 'linksupdatetest', 'Foo' ), + ) ); + } + + public function testUpdate_templatelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 ); + + $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array( + array( NS_TEMPLATE, 'Foo' ), + ) ); + } + + public function testUpdate_imagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addImage( "Foo.png" ); + + + $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array( + array( 'Foo.png' ), + ) ); + } + + public function testUpdate_langlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLanguageLink( Title::newFromText( "en:Foo" ) ); + + + $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array( + array( 'En', 'Foo' ), + ) ); + } + + public function testUpdate_page_props() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->setProperty( "foo", "bar" ); + + $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array( + array( 'foo', 'bar' ), + ) ); + } + + #@todo: test recursive, too! + + protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) { + $update = new LinksUpdate( $title, $parserOutput ); + + $update->doUpdate(); + + $this->assertSelect( $table, $fields, $condition, $expectedRows ); + } +} + diff --git a/tests/phpunit/includes/LocalisationCacheTest.php b/tests/phpunit/includes/LocalisationCacheTest.php new file mode 100644 index 00000000..356db87c --- /dev/null +++ b/tests/phpunit/includes/LocalisationCacheTest.php @@ -0,0 +1,31 @@ +<?php + +class LocalisationCacheTest extends MediaWikiTestCase { + public function testPuralRulesFallback() { + $cache = Language::getLocalisationCache(); + + $this->assertEquals( + $cache->getItem( 'ru', 'pluralRules' ), + $cache->getItem( 'os', 'pluralRules' ), + 'os plural rules (undefined) fallback to ru (defined)' + ); + + $this->assertEquals( + $cache->getItem( 'ru', 'compiledPluralRules' ), + $cache->getItem( 'os', 'compiledPluralRules' ), + 'os compiled plural rules (undefined) fallback to ru (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'pluralRules' ), + $cache->getItem( 'de', 'pluralRules' ), + 'ksh plural rules (defined) dont fallback to de (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'compiledPluralRules' ), + $cache->getItem( 'de', 'compiledPluralRules' ), + 'ksh compiled plural rules (defined) dont fallback to de (defined)' + ); + } +} diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php index 6b231fc5..3b05d675 100644 --- a/tests/phpunit/includes/MWNamespaceTest.php +++ b/tests/phpunit/includes/MWNamespaceTest.php @@ -53,10 +53,6 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertIsNotSubject( NS_TALK ); $this->assertIsNotSubject( NS_USER_TALK ); $this->assertIsNotSubject( 101 ); # user defined - - // Back compat - $this->assertTrue( MWNamespace::isMain( NS_MAIN ) == MWNamespace::isSubject( NS_MAIN ) ); - $this->assertTrue( MWNamespace::isMain( NS_USER_TALK ) == MWNamespace::isSubject( NS_USER_TALK ) ); } /** @@ -442,6 +438,36 @@ class MWNamespaceTest extends MediaWikiTestCase { } /** + */ + public function testGetSubjectNamespaces() { + $subjectsNS = MWNamespace::getSubjectNamespaces(); + $this->assertContains( NS_MAIN, $subjectsNS, + "Talk namespaces should have NS_MAIN" ); + $this->assertNotContains( NS_TALK, $subjectsNS, + "Talk namespaces should have NS_TALK" ); + + $this->assertNotContains( NS_MEDIA, $subjectsNS, + "Talk namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $subjectsNS, + "Talk namespaces should not have NS_SPECIAL" ); + } + + /** + */ + public function testGetTalkNamespaces() { + $talkNS = MWNamespace::getTalkNamespaces(); + $this->assertContains( NS_TALK, $talkNS, + "Subject namespaces should have NS_TALK" ); + $this->assertNotContains( NS_MAIN, $talkNS, + "Subject namespaces should not have NS_MAIN" ); + + $this->assertNotContains( NS_MEDIA, $talkNS, + "Subject namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $talkNS, + "Subject namespaces should not have NS_SPECIAL" ); + } + + /** * Some namespaces are always capitalized per code definition * in MWNamespace::$alwaysCapitalizedNamespaces */ @@ -550,6 +576,15 @@ class MWNamespaceTest extends MediaWikiTestCase { } + public function testIsNonincludable() { + global $wgNonincludableNamespaces; + $wgNonincludableNamespaces = array( NS_USER ); + + $this->assertTrue( MWNamespace::isNonincludable( NS_USER ) ); + + $this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) ); + } + ####### HELPERS ########################################################### function __call( $method, $args ) { // Call the real method if it exists diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index 295b6d74..20181fd4 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -16,6 +16,8 @@ class MessageTest extends MediaWikiLangTestCase { $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) ); $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() ); $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->text() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->plain() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->escaped() ); } function testInLanguage() { diff --git a/tests/phpunit/includes/PreferencesTest.php b/tests/phpunit/includes/PreferencesTest.php new file mode 100644 index 00000000..0e123177 --- /dev/null +++ b/tests/phpunit/includes/PreferencesTest.php @@ -0,0 +1,75 @@ +<?php + +class PreferencesTest extends MediaWikiTestCase { + /** Array of User objects */ + private $prefUsers; + private $context; + + function __construct() { + parent::__construct(); + global $wgEnableEmail; + + $this->prefUsers['noemail'] = new User; + + $this->prefUsers['notauth'] = new User; + $this->prefUsers['notauth'] + ->setEmail( 'noauth@example.org' ); + + $this->prefUsers['auth'] = new User; + $this->prefUsers['auth'] + ->setEmail( 'noauth@example.org' ); + $this->prefUsers['auth'] + ->setEmailAuthenticationTimestamp( 1330946623 ); + + $this->context = new RequestContext; + $this->context->setTitle( Title::newFromText('PreferencesTest') ); + + //some tests depends on email setting + $wgEnableEmail = true; + } + + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserHasNoEmail() { + $prefs = $this->prefsFor( 'noemail' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-none', $prefs['emailaddress']['cssclass'] ); + } + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserEmailNotAuthenticated() { + $prefs = $this->prefsFor( 'notauth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-not-authenticated', $prefs['emailaddress']['cssclass'] ); + } + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + function testEmailFieldsWhenUserEmailIsAuthenticated() { + $prefs = $this->prefsFor( 'auth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-authenticated', $prefs['emailaddress']['cssclass'] ); + } + + /** Helper */ + function prefsFor( $user_key ) { + $preferences = array(); + Preferences::profilePreferences( + $this->prefUsers[$user_key] + , $this->context + , $preferences + ); + return $preferences; + } +} diff --git a/tests/phpunit/includes/RecentChangeTest.php b/tests/phpunit/includes/RecentChangeTest.php new file mode 100644 index 00000000..fbf271cc --- /dev/null +++ b/tests/phpunit/includes/RecentChangeTest.php @@ -0,0 +1,273 @@ +<?php +/** + * @group Database + */ +class RecentChangeTest extends MediaWikiTestCase { + protected $title; + protected $target; + protected $user; + protected $user_comment; + protected $context; + + function __construct() { + parent::__construct(); + + $this->title = Title::newFromText( 'SomeTitle' ); + $this->target = Title::newFromText( 'TestTarget' ); + $this->user = User::newFromName( 'UserName' ); + + $this->user_comment = '<User comment about action>'; + $this->context = RequestContext::newExtraneousContext( $this->title ); + } + + /** + * The testIrcMsgForAction* tests are supposed to cover the hacky + * LogFormatter::getIRCActionText / bug 34508 + * + * Third parties bots listen to those messages. They are clever enough + * to fetch the i18n messages from the wiki and then analyze the IRC feed + * to reverse engineer the $1, $2 messages. + * One thing bots can not detect is when MediaWiki change the meaning of + * a message like what happened when we deployed 1.19. $1 became the user + * performing the action which broke basically all bots around. + * + * Should cover the following log actions (which are most commonly used by bots): + * - block/block + * - block/unblock + * - delete/delete + * - delete/restore + * - newusers/create + * - newusers/create2 + * - newusers/autocreate + * - move/move + * - move/move_redir + * - protect/protect + * - protect/modifyprotect + * - protect/unprotect + * - upload/upload + * + * As well as the following Auto Edit Summaries: + * - blank + * - replace + * - rollback + * - undo + */ + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeBlock() { + # block/block + $this->assertIRCComment( + $this->context->msg( 'blocklogentry', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'block', 'block', + array(), + $this->user_comment + ); + # block/unblock + $this->assertIRCComment( + $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'block', 'unblock', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeDelete() { + # delete/delete + $this->assertIRCComment( + $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'delete', 'delete', + array(), + $this->user_comment + ); + + # delete/restore + $this->assertIRCComment( + $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'delete', 'restore', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeNewusers() { + $this->assertIRCComment( + 'New user account', + 'newusers', 'newusers', + array() + ); + $this->assertIRCComment( + 'New user account', + 'newusers', 'create', + array() + ); + $this->assertIRCComment( + 'created new account SomeTitle', + 'newusers', 'create2', + array() + ); + $this->assertIRCComment( + 'Account created automatically', + 'newusers', 'autocreate', + array() + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeMove() { + $move_params = array( + '4::target' => $this->target->getPrefixedText(), + '5::noredir' => 0, + ); + + # move/move + $this->assertIRCComment( + $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )->plain() . ': ' . $this->user_comment, + 'move', 'move', + $move_params, + $this->user_comment + ); + + # move/move_redir + $this->assertIRCComment( + $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )->plain() . ': ' . $this->user_comment, + 'move', 'move_redir', + $move_params, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypePatrol() { + # patrol/patrol + $this->assertIRCComment( + $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(), + 'patrol', 'patrol', + array( + '4::curid' => '777', + '5::previd' => '666', + '6::auto' => 0, + ) + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeProtect() { + $protectParams = array( + '[edit=sysop] (indefinite) [move=sysop] (indefinite)' + ); + + # protect/protect + $this->assertIRCComment( + $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] )->plain() . ': ' . $this->user_comment, + 'protect', 'protect', + $protectParams, + $this->user_comment + ); + + # protect/unprotect + $this->assertIRCComment( + $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'protect', 'unprotect', + array(), + $this->user_comment + ); + + # protect/modify + $this->assertIRCComment( + $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] )->plain() . ': ' . $this->user_comment, + 'protect', 'modify', + $protectParams, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + function testIrcMsgForLogTypeUpload() { + # upload/upload + $this->assertIRCComment( + $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'upload', 'upload', + array(), + $this->user_comment + ); + + # upload/overwrite + $this->assertIRCComment( + $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . ': ' . $this->user_comment, + 'upload', 'overwrite', + array(), + $this->user_comment + ); + } + + /** + * @todo: Emulate these edits somehow and extract + * raw edit summary from RecentChange object + * -- + + function testIrcMsgForBlankingAES() { + // $this->context->msg( 'autosumm-blank', .. ); + } + + function testIrcMsgForReplaceAES() { + // $this->context->msg( 'autosumm-replace', .. ); + } + + function testIrcMsgForRollbackAES() { + // $this->context->msg( 'revertpage', .. ); + } + + function testIrcMsgForUndoAES() { + // $this->context->msg( 'undo-summary', .. ); + } + + * -- + */ + + /** + * @param $expected String Expected IRC text without colors codes + * @param $type String Log type (move, delete, suppress, patrol ...) + * @param $action String A log type action + * @param $comment String (optional) A comment for the log action + * @param $msg String (optional) A message for PHPUnit :-) + */ + function assertIRCComment( $expected, $type, $action, $params, $comment = null, $msg = '' ) { + + $logEntry = new ManualLogEntry( $type, $action ); + $logEntry->setPerformer( $this->user ); + $logEntry->setTarget( $this->title ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + $logEntry->setParameters( $params ); + + $formatter = LogFormatter::newFromEntry( $logEntry ); + $formatter->setContext( $this->context ); + + // Apply the same transformation as done in RecentChange::getIRCLine for rc_comment + $ircRcComment = RecentChange::cleanupForIRC( $formatter->getIRCActionComment() ); + + $this->assertEquals( + $expected, + $ircRcComment, + $msg + ); + } + +} diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php new file mode 100644 index 00000000..8a7facec --- /dev/null +++ b/tests/phpunit/includes/RevisionStorageTest.php @@ -0,0 +1,408 @@ +<?php + +/** + * Test class for Revision storage. + * + * @group Database + * ^--- important, causes temporary tables to be used instead of the real database + * + * @group medium + * ^--- important, causes tests not to fail with timeout + */ +class RevisionStorageTest extends MediaWikiTestCase { + + var $the_page; + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge( $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + public function setUp() { + if ( !$this->the_page ) { + $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" ); + } + } + + protected function makeRevision( $props = null ) { + if ( $props === null ) $props = array(); + + if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) $props['text'] = 'Lorem Ipsum'; + if ( !isset( $props['comment'] ) ) $props['comment'] = 'just a test'; + if ( !isset( $props['page'] ) ) $props['page'] = $this->the_page->getId(); + + $rev = new Revision( $props ); + + $dbw = wfgetDB( DB_MASTER ); + $rev->insertOn( $dbw ); + + return $rev; + } + + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) ) $page = Title::newFromText( $page ); + if ( $page instanceof Title ) $page = new WikiPage( $page ); + + if ( $page->exists() ) { + $page->doDeleteArticle( "done" ); + } + + $page->doEdit( $text, "testing", EDIT_NEW ); + + return $page; + } + + protected function assertRevEquals( Revision $orig, Revision $rev = null ) { + $this->assertNotNull( $rev, 'missing revision' ); + + $this->assertEquals( $orig->getId(), $rev->getId() ); + $this->assertEquals( $orig->getPage(), $rev->getPage() ); + $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() ); + $this->assertEquals( $orig->getUser(), $rev->getUser() ); + $this->assertEquals( $orig->getSha1(), $rev->getSha1() ); + } + + /** + * @covers Revision::__construct + */ + public function testConstructFromRow() + { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = new Revision( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromRow + */ + public function testNewFromRow() + { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + + /** + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRow() + { + $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum' ); + $orig = $page->getRevision(); + $page->doDeleteArticle( 'test Revision::newFromArchiveRow' ); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'archive', '*', array( 'ar_rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromArchiveRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromId + */ + public function testNewFromId() + { + $orig = $this->makeRevision(); + + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::fetchRevision + */ + public function testFetchRevision() + { + $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' ); + $id1 = $page->getRevision()->getId(); + + $page->doEdit( 'two', 'second rev' ); + $id2 = $page->getRevision()->getId(); + + $res = Revision::fetchRevision( $page->getTitle() ); + + #note: order is unspecified + $rows = array(); + while ( ( $row = $res->fetchObject() ) ) { + $rows[ $row->rev_id ]= $row; + } + + $row = $res->fetchObject(); + $this->assertEquals( 1, count($rows), 'expected exactly one revision' ); + $this->assertArrayHasKey( $id2, $rows, 'missing revision with id ' . $id2 ); + } + + /** + * @covers Revision::selectFields + */ + public function testSelectFields() + { + $fields = Revision::selectFields(); + + $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields'); + $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields'); + $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields'); + $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields'); + } + + /** + * @covers Revision::getPage + */ + public function testGetPage() + { + $page = $this->the_page; + + $orig = $this->makeRevision( array( 'page' => $page->getId() ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( $page->getId(), $rev->getPage() ); + } + + /** + * @covers Revision::getText + */ + public function testGetText() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello.', $rev->getText() ); + } + + /** + * @covers Revision::revText + */ + public function testRevText() + { + $this->hideDeprecated( 'Revision::revText' ); + $orig = $this->makeRevision( array( 'text' => 'hello hello rev.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello rev.', $rev->revText() ); + } + + /** + * @covers Revision::getRawText + */ + public function testGetRawText() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello raw.', $rev->getRawText() ); + } + /** + * @covers Revision::isCurrent + */ + public function testIsCurrent() + { + $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum' ); + $rev1 = $page->getRevision(); + + # @todo: find out if this should be true + # $this->assertTrue( $rev1->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertTrue( $rev1x->isCurrent() ); + + $page->doEdit( 'Bla bla', 'second rev' ); + $rev2 = $page->getRevision(); + + # @todo: find out if this should be true + # $this->assertTrue( $rev2->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertFalse( $rev1x->isCurrent() ); + + $rev2x = Revision::newFromId( $rev2->getId() ); + $this->assertTrue( $rev2x->isCurrent() ); + } + + /** + * @covers Revision::getPrevious + */ + public function testGetPrevious() + { + $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious' ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getPrevious() ); + + $page->doEdit( 'Bla bla', 'second rev testGetPrevious' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev2->getPrevious() ); + $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() ); + } + + /** + * @covers Revision::getNext + */ + public function testGetNext() + { + $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext' ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getNext() ); + + $page->doEdit( 'Bla bla', 'second rev testGetNext' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev1->getNext() ); + $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() ); + } + + /** + * @covers Revision::newNullRevision + */ + public function testNewNullRevision() + { + $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text' ); + $orig = $page->getRevision(); + + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false ); + + $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' ); + $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' ); + $this->assertEquals( 'some testing text', $rev->getText() ); + } + + public function dataUserWasLastToEdit() { + return array( + array( #0 + 3, true, # actually the last edit + ), + array( #1 + 2, true, # not the current edit, but still by this user + ), + array( #2 + 1, false, # edit by another user + ), + array( #3 + 0, false, # first edit, by this user, but another user edited in the mean time + ), + ); + } + + /** + * @dataProvider dataUserWasLastToEdit + */ + public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) { + $userA = \User::newFromName( "RevisionStorageTest_userA" ); + $userB = \User::newFromName( "RevisionStorageTest_userB" ); + + if ( $userA->getId() === 0 ) { + $userA = \User::createNew( $userA->getName() ); + } + + if ( $userB->getId() === 0 ) { + $userB = \User::createNew( $userB->getName() ); + } + + $dbw = wfGetDB( DB_MASTER ); + $revisions = array(); + + // create revisions ----------------------------- + $page = WikiPage::factory( Title::newFromText( 'RevisionStorageTest_testUserWasLastToEdit' ) ); + + # zero + $revisions[0] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000000', + 'user' => $userA->getId(), + 'text' => 'zero', + 'summary' => 'edit zero' + ) ); + $revisions[0]->insertOn( $dbw ); + + # one + $revisions[1] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000100', + 'user' => $userA->getId(), + 'text' => 'one', + 'summary' => 'edit one' + ) ); + $revisions[1]->insertOn( $dbw ); + + # two + $revisions[2] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000200', + 'user' => $userB->getId(), + 'text' => 'two', + 'summary' => 'edit two' + ) ); + $revisions[2]->insertOn( $dbw ); + + # three + $revisions[3] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000300', + 'user' => $userA->getId(), + 'text' => 'three', + 'summary' => 'edit three' + ) ); + $revisions[3]->insertOn( $dbw ); + + # four + $revisions[4] = new Revision( array( + 'page' => $page->getId(), + 'timestamp' => '20120101000200', + 'user' => $userA->getId(), + 'text' => 'zero', + 'summary' => 'edit four' + ) ); + $revisions[4]->insertOn( $dbw ); + + // test it --------------------------------- + $since = $revisions[ $sinceIdx ]->getTimestamp(); + + $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since ); + + $this->assertEquals( $expectedLast, $wasLast ); + } +} diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php index 77a371d5..59ba0a04 100644 --- a/tests/phpunit/includes/SampleTest.php +++ b/tests/phpunit/includes/SampleTest.php @@ -47,7 +47,7 @@ class TestSample extends MediaWikiLangTestCase { array( 'Text', null, 'Text' ), array( 'text', null, 'Text' ), array( 'Text', NS_USER, 'User:Text' ), - array( 'Photo.jpg', NS_IMAGE, 'File:Photo.jpg' ) + array( 'Photo.jpg', NS_FILE, 'File:Photo.jpg' ) ); } diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php index b76aa5c7..66af2581 100644 --- a/tests/phpunit/includes/SanitizerTest.php +++ b/tests/phpunit/includes/SanitizerTest.php @@ -110,21 +110,27 @@ class SanitizerTest extends MediaWikiTestCase { $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&foobar;' ), array( 'foo' => '&foobar;' ), 'Entity-like items are accepted' ); } - function testDeprecatedAttributes() { - $GLOBALS['wgCleanupPresentationalAttributes'] = true; - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="left"', 'br' ), ' style="clear: left;"', 'Deprecated attributes are converted to styles when enabled.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="all"', 'br' ), ' style="clear: both;"', 'clear=all is converted to clear: both; not clear: all;' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'CLEAR="ALL"', 'br' ), ' style="clear: both;"', 'clear=ALL is not treated differently from clear=all' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'width="100"', 'td' ), ' style="width: 100px;"', 'Numeric sizes use pixels instead of numbers.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'width="100%"', 'td' ), ' style="width: 100%;"', 'Units are allowed in sizes.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'WIDTH="100%"', 'td' ), ' style="width: 100%;"', 'Uppercase WIDTH is treated as lowercase width.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'WiDTh="100%"', 'td' ), ' style="width: 100%;"', 'Mixed case does not break WiDTh.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'nowrap="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute is output as white-space: nowrap; not something else.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'nowrap=""', 'td' ), ' style="white-space: nowrap;"', 'nowrap="" is considered true, not false' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'NOWRAP="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute works when uppercase.' ); - $this->assertEquals( Sanitizer::fixTagAttributes( 'NoWrAp="true"', 'td' ), ' style="white-space: nowrap;"', 'nowrap attribute works when mixed-case.' ); - $GLOBALS['wgCleanupPresentationalAttributes'] = false; - $this->assertEquals( Sanitizer::fixTagAttributes( 'clear="left"', 'br' ), ' clear="left"', 'Deprecated attributes are not converted to styles when enabled.' ); + /** + * @dataProvider provideDeprecatedAttributes + */ + function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl ) { + $this->assertEquals( " $inputAttr", Sanitizer::fixTagAttributes( $inputAttr, $inputEl ) ); + } + + public static function provideDeprecatedAttributes() { + return array( + array( 'clear="left"', 'br' ), + array( 'clear="all"', 'br' ), + array( 'width="100"', 'td' ), + array( 'nowrap="true"', 'td' ), + array( 'nowrap=""', 'td' ), + array( 'align="right"', 'td' ), + array( 'align="center"', 'table' ), + array( 'align="left"', 'tr' ), + array( 'align="center"', 'div' ), + array( 'align="left"', 'h1' ), + array( 'align="left"', 'span' ), + ); } /** diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php index de9d6dc6..39ce6e31 100644 --- a/tests/phpunit/includes/TemplateCategoriesTest.php +++ b/tests/phpunit/includes/TemplateCategoriesTest.php @@ -3,26 +3,24 @@ /** * @group Database */ -require dirname( __FILE__ ) . "/../../../maintenance/runJobs.php"; +require __DIR__ . "/../../../maintenance/runJobs.php"; class TemplateCategoriesTest extends MediaWikiLangTestCase { function testTemplateCategories() { - global $wgUser; - $title = Title::newFromText( "Categorized from template" ); - $article = new Article( $title ); - $wgUser = new User(); - $wgUser->mRights['*'] = array( 'createpage', 'edit', 'purge' ); + $page = WikiPage::factory( $title ); + $user = new User(); + $user->mRights = array( 'createpage', 'edit', 'purge' ); - $status = $article->doEdit( '{{Categorising template}}', 'Create a page with a template', 0 ); + $status = $page->doEdit( '{{Categorising template}}', 'Create a page with a template', 0, false, $user ); $this->assertEquals( array() , $title->getParentCategories() ); - $template = new Article( Title::newFromText( 'Template:Categorising template' ) ); - $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0 ); + $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) ); + $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0, false, $user ); // Run the job queue $jobs = new RunJobs; diff --git a/tests/phpunit/includes/api/ApiTestUser.php b/tests/phpunit/includes/TestUser.php index 8d5f61a7..c4d89455 100644 --- a/tests/phpunit/includes/api/ApiTestUser.php +++ b/tests/phpunit/includes/TestUser.php @@ -1,7 +1,7 @@ <?php /* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */ -class ApiTestUser { +class TestUser { public $username; public $password; public $email; @@ -55,5 +55,4 @@ class ApiTestUser { $this->user->saveSettings(); } - } diff --git a/tests/phpunit/includes/TimestampTest.php b/tests/phpunit/includes/TimestampTest.php new file mode 100644 index 00000000..231228f5 --- /dev/null +++ b/tests/phpunit/includes/TimestampTest.php @@ -0,0 +1,72 @@ +<?php + +/** + * Tests timestamp parsing and output. + */ +class TimestampTest extends MediaWikiTestCase { + /** + * Test parsing of valid timestamps and outputing to MW format. + * @dataProvider provideValidTimestamps + */ + function testValidParse( $format, $original, $expected ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) ); + } + + /** + * Test outputting valid timestamps to different formats. + * @dataProvider provideValidTimestamps + */ + function testValidOutput( $format, $expected, $original ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, (string) $timestamp->getTimestamp( $format ) ); + } + + /** + * Test an invalid timestamp. + * @expectedException TimestampException + */ + function testInvalidParse() { + $timestamp = new MWTimestamp( "This is not a timestamp." ); + } + + /** + * Test requesting an invalid output format. + * @expectedException TimestampException + */ + function testInvalidOutput() { + $timestamp = new MWTimestamp( '1343761268' ); + $timestamp->getTimestamp( 98 ); + } + + /** + * Test human readable timestamp format. + */ + function testHumanOutput() { + $timestamp = new MWTimestamp( time() - 3600 ); + $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->toString() ); + } + + /** + * Returns a list of valid timestamps in the format: + * array( type, timestamp_of_type, timestamp_in_MW ) + */ + function provideValidTimestamps() { + return array( + // Various formats + array( TS_UNIX, '1343761268', '20120731190108' ), + array( TS_MW, '20120731190108', '20120731190108' ), + array( TS_DB, '2012-07-31 19:01:08', '20120731190108' ), + array( TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ), + array( TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ), + array( TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ), + array( TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ), + array( TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ), + array( TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ), + array( TS_DB2, '2012-07-31 19:01:08', '20120731190108' ), + // Some extremes and weird values + array( TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ), + array( TS_UNIX, '-62135596801', '00001231235959' ) + ); + } +} diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index 2f1103e8..aed658ba 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -21,8 +21,8 @@ class TitleMethodsTest extends MediaWikiTestCase { $titleA = Title::newFromText( $titleA ); $titleB = Title::newFromText( $titleB ); - $this->assertEquals( $titleA->equals( $titleB ), $expectedBool ); - $this->assertEquals( $titleB->equals( $titleA ), $expectedBool ); + $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) ); + $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) ); } public function dataInNamespace() { @@ -43,7 +43,7 @@ class TitleMethodsTest extends MediaWikiTestCase { */ public function testInNamespace( $title, $ns, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $title->inNamespace( $ns ), $expectedBool ); + $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) ); } public function testInNamespaces() { @@ -72,7 +72,130 @@ class TitleMethodsTest extends MediaWikiTestCase { */ public function testHasSubjectNamespace( $title, $ns, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $title->hasSubjectNamespace( $ns ), $expectedBool ); + $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) ); + } + + public function dataIsCssOrJsPage() { + return array( + array( 'Foo', false ), + array( 'Foo.js', false ), + array( 'Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', true ), + array( 'MediaWiki:Foo.css', true ), + array( 'MediaWiki:Foo.JS', false ), + array( 'MediaWiki:Foo.CSS', false ), + array( 'MediaWiki:Foo.css.xxx', false ), + ); + } + + /** + * @dataProvider dataIsCssOrJsPage + */ + public function testIsCssOrJsPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssOrJsPage() ); + } + + + public function dataIsCssJsSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.js', false ), + array( 'Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', true ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', false ), + array( 'User:Foo/bar.JS', false ), + array( 'User:Foo/bar.CSS', false ), + ); + } + + /** + * @dataProvider dataIsCssJsSubpage + */ + public function testIsCssJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssJsSubpage() ); + } + + public function dataIsCssSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', true ), + ); + } + + /** + * @dataProvider dataIsCssSubpage + */ + public function testIsCssSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssSubpage() ); + } + + public function dataIsJsSubpage() { + return array( + array( 'Foo', false ), + array( 'Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', false ), + ); + } + + /** + * @dataProvider dataIsJsSubpage + */ + public function testIsJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isJsSubpage() ); + } + + public function dataIsWikitextPage() { + return array( + array( 'Foo', true ), + array( 'Foo.js', true ), + array( 'Foo/bar.js', true ), + array( 'User:Foo', true ), + array( 'User:Foo.js', true ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', true ), + array( 'User:Foo/bar.js.xxx', true ), + array( 'User:Foo/bar.xxx', true ), + array( 'MediaWiki:Foo.js', false ), + array( 'MediaWiki:Foo.css', false ), + array( 'MediaWiki:Foo/bar.css', false ), + array( 'User:Foo/bar.JS', true ), + array( 'User:Foo/bar.CSS', true ), + ); + } + + /** + * @dataProvider dataIsWikitextPage + */ + public function testIsWikitextPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isWikitextPage() ); } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 1c8be5f9..f61652df 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -77,4 +77,79 @@ class TitleTest extends MediaWikiTestCase { } + /** + * @dataProvider provideCasesForGetpageviewlanguage + */ + function testGetpageviewlanguage( $expected, $titleText, $contLang, $lang, $variant, $msg='' ) { + // Save globals + global $wgContLang, $wgLang, $wgAllowUserJs, $wgLanguageCode, $wgDefaultLanguageVariant; + $save['wgContLang'] = $wgContLang; + $save['wgLang'] = $wgLang; + $save['wgAllowUserJs'] = $wgAllowUserJs; + $save['wgLanguageCode'] = $wgLanguageCode; + $save['wgDefaultLanguageVariant'] = $wgDefaultLanguageVariant; + + // Setup test environnement: + $wgContLang = Language::factory( $contLang ); + $wgLang = Language::factory( $lang ); + # To test out .js titles: + $wgAllowUserJs = true; + $wgLanguageCode = $contLang; + $wgDefaultLanguageVariant = $variant; + + $title = Title::newFromText( $titleText ); + $this->assertInstanceOf( 'Title', $title, + "Test must be passed a valid title text, you gave '$titleText'" + ); + $this->assertEquals( $expected, + $title->getPageViewLanguage()->getCode(), + $msg + ); + + // Restore globals + $wgContLang = $save['wgContLang']; + $wgLang = $save['wgLang']; + $wgAllowUserJs = $save['wgAllowUserJs']; + $wgLanguageCode = $save['wgLanguageCode']; + $wgDefaultLanguageVariant = $save['wgDefaultLanguageVariant']; + } + + function provideCasesForGetpageviewlanguage() { + # Format: + # - expected + # - Title name + # - wgContLang (expected in most case) + # - wgLang (on some specific pages) + # - wgDefaultLanguageVariant + # - Optional message + return array( + array( 'fr', 'Main_page', 'fr', 'fr', false ), + array( 'es', 'Main_page', 'es', 'zh-tw', false ), + array( 'zh', 'Main_page', 'zh', 'zh-tw', false ), + + array( 'es', 'Main_page', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ), + + array( 'zh-cn', 'Main_page', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ), + + array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ), + + ); + } } diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php index ef03e835..7a424aef 100644 --- a/tests/phpunit/includes/UserTest.php +++ b/tests/phpunit/includes/UserTest.php @@ -140,4 +140,32 @@ class UserTest extends MediaWikiTestCase { array( 'Ab cd', false, ' Ideographic space' ), ); } + + /** + * Test, if for all rights a right- message exist, + * which is used on Special:ListGroupRights as help text + * Extensions and core + */ + public function testAllRightsWithMessage() { + //Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights + $allRights = User::getAllRights(); + $allMessageKeys = Language::getMessageKeysFor( 'en' ); + + $rightsWithMessage = array(); + foreach ( $allMessageKeys as $message ) { + // === 0: must be at beginning of string (position 0) + if ( strpos( $message, 'right-' ) === 0 ) { + $rightsWithMessage[] = substr( $message, strlen( 'right-' ) ); + } + } + + sort( $allRights ); + sort( $rightsWithMessage ); + + $this->assertEquals( + $allRights, + $rightsWithMessage, + 'Each user rights (core/extensions) has a corresponding right- message.' + ); + } } diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php index e72408f6..1fc0b4b3 100644 --- a/tests/phpunit/includes/WebRequestTest.php +++ b/tests/phpunit/includes/WebRequestTest.php @@ -1,14 +1,22 @@ <?php class WebRequestTest extends MediaWikiTestCase { + static $oldServer; + + function setUp() { + self::$oldServer = $_SERVER; + } + + function tearDown() { + $_SERVER = self::$oldServer; + } + /** * @dataProvider provideDetectServer */ function testDetectServer( $expected, $input, $description ) { - $oldServer = $_SERVER; $_SERVER = $input; $result = WebRequest::detectServer(); - $_SERVER = $oldServer; $this->assertEquals( $expected, $result, $description ); } @@ -91,13 +99,11 @@ class WebRequestTest extends MediaWikiTestCase { */ function testGetIP( $expected, $input, $squid, $private, $description ) { global $wgSquidServersNoPurge, $wgUsePrivateIPs; - $oldServer = $_SERVER; $_SERVER = $input; $wgSquidServersNoPurge = $squid; $wgUsePrivateIPs = $private; $request = new WebRequest(); $result = $request->getIP(); - $_SERVER = $oldServer; $this->assertEquals( $expected, $result, $description ); } @@ -182,4 +188,29 @@ class WebRequestTest extends MediaWikiTestCase { # Next call throw an exception about lacking an IP $request->getIP(); } + + function languageProvider() { + return array( + array( '', array(), 'Empty Accept-Language header' ), + array( 'en', array( 'en' => 1 ), 'One language' ), + array( 'en, ar', array( 'en' => 1, 'ar' => 1 ), 'Two languages listed in appearance order.' ), + array( 'zh-cn,zh-tw', array( 'zh-cn' => 1, 'zh-tw' => 1 ), 'Two equally prefered languages, listed in appearance order per rfc3282. Checks c9119' ), + array( 'es, en; q=0.5', array( 'es' => 1, 'en' => '0.5' ), 'Spanish as first language and English and second' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Less prefered language first' ), + array( 'fr, en; q=0.5, es', array( 'fr' => 1, 'es' => 1, 'en' => '0.5' ), 'Three languages' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Two languages' ), + array( 'en, zh;q=0', array( 'en' => 1 ), "It's Chinese to me" ), + array( 'es; q=1, pt;q=0.7, it; q=0.6, de; q=0.1, ru;q=0', array( 'es' => '1', 'pt' => '0.7', 'it' => '0.6', 'de' => '0.1' ), 'Preference for romance languages' ), + array( 'en-gb, en-us; q=1', array( 'en-gb' => 1, 'en-us' => '1' ), 'Two equally prefered English variants' ), + ); + } + + /** + * @dataProvider languageProvider + */ + function testAcceptLang($acceptLanguageHeader, $expectedLanguages, $description) { + $_SERVER = array( 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader ); + $request = new WebRequest(); + $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description); + } } diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php new file mode 100644 index 00000000..0e1e1ce8 --- /dev/null +++ b/tests/phpunit/includes/WikiPageTest.php @@ -0,0 +1,784 @@ +<?php +/** +* @group Database +* ^--- important, causes temporary tables to be used instead of the real database +* @group medium +**/ + +class WikiPageTest extends MediaWikiLangTestCase { + + var $pages_to_delete; + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge ( $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + public function setUp() { + parent::setUp(); + $this->pages_to_delete = array(); + } + + public function tearDown() { + foreach ( $this->pages_to_delete as $p ) { + /* @var $p WikiPage */ + + try { + if ( $p->exists() ) { + $p->doDeleteArticle( "testing done." ); + } + } catch ( MWException $ex ) { + // fail silently + } + } + parent::tearDown(); + } + + protected function newPage( $title ) { + if ( is_string( $title ) ) $title = Title::newFromText( $title ); + + $p = new WikiPage( $title ); + + $this->pages_to_delete[] = $p; + + return $p; + } + + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) ) $page = Title::newFromText( $page ); + if ( $page instanceof Title ) $page = $this->newPage( $page ); + + $page->doEdit( $text, "testing", EDIT_NEW ); + + return $page; + } + + public function testDoEdit() { + $title = Title::newFromText( "WikiPageTest_testDoEdit" ); + + $page = $this->newPage( $title ); + + $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat."; + + $page->doEdit( $text, "testing 1" ); + + $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); + $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); + + $id = $page->getId(); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. " + . "Stet clita kasd [[gubergren]], no sea takimata sanctus est."; + + $page->doEdit( $text, "testing 2" ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + + public function testDoQuickEdit() { + global $wgUser; + + $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" ); + + $text = "quick text"; + $page->doQuickEdit( $text, $wgUser, "testing q" ); + + # --------------------- + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $text, $page->getText() ); + } + + public function testDoDeleteArticle() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" ); + $id = $page->getId(); + + $page->doDeleteArticle( "testing deletion" ); + + $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" ); + $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" ); + + $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); + $this->assertFalse( $t->exists(), "Title::exists should return false after page was deleted" ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + public function testDoDeleteUpdates() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" ); + $id = $page->getId(); + + $page->doDeleteUpdates( $id ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + public function testGetRevision() { + $page = $this->newPage( "WikiPageTest_testGetRevision" ); + + $rev = $page->getRevision(); + $this->assertNull( $rev ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $rev = $page->getRevision(); + + $this->assertEquals( $page->getLatest(), $rev->getId() ); + $this->assertEquals( "some text", $rev->getText() ); + } + + public function testGetText() { + $page = $this->newPage( "WikiPageTest_testGetText" ); + + $text = $page->getText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $text = $page->getText(); + $this->assertEquals( "some text", $text ); + } + + public function testGetRawText() { + $page = $this->newPage( "WikiPageTest_testGetRawText" ); + + $text = $page->getRawText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $text = $page->getRawText(); + $this->assertEquals( "some text", $text ); + } + + + public function testExists() { + $page = $this->newPage( "WikiPageTest_testExists" ); + $this->assertFalse( $page->exists() ); + + # ----------------- + $this->createPage( $page, "some text" ); + $this->assertTrue( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->exists() ); + + # ----------------- + $page->doDeleteArticle( "done testing" ); + $this->assertFalse( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertFalse( $page->exists() ); + } + + public function dataHasViewableContent() { + return array( + array( 'WikiPageTest_testHasViewableContent', false, true ), + array( 'Special:WikiPageTest_testHasViewableContent', false ), + array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ), + array( 'Special:Userlogin', true ), + array( 'MediaWiki:help', true ), + ); + } + + /** + * @dataProvider dataHasViewableContent + */ + public function testHasViewableContent( $title, $viewable, $create = false ) { + $page = $this->newPage( $title ); + $this->assertEquals( $viewable, $page->hasViewableContent() ); + + if ( $create ) { + $this->createPage( $page, "some text" ); + $this->assertTrue( $page->hasViewableContent() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->hasViewableContent() ); + } + } + + public function dataGetRedirectTarget() { + return array( + array( 'WikiPageTest_testGetRedirectTarget_1', "hello world", null ), + array( 'WikiPageTest_testGetRedirectTarget_2', "#REDIRECT [[hello world]]", "Hello world" ), + ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function testGetRedirectTarget( $title, $text, $target ) { + $page = $this->createPage( $title, $text ); + + # now, test the actual redirect + $t = $page->getRedirectTarget(); + $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function testIsRedirect( $title, $text, $target ) { + $page = $this->createPage( $title, $text ); + $this->assertEquals( !is_null( $target ), $page->isRedirect() ); + } + + public function dataIsCountable() { + return array( + + // any + array( 'WikiPageTest_testIsCountable', + '', + 'any', + true + ), + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'any', + true + ), + + // comma + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + 'Foo, bar', + 'comma', + true + ), + + // link + array( 'WikiPageTest_testIsCountable', + 'Foo', + 'link', + false + ), + array( 'WikiPageTest_testIsCountable', + 'Foo [[bar]]', + 'link', + true + ), + + // redirects + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'any', + false + ), + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + '#REDIRECT [[bar]]', + 'link', + false + ), + + // not a content namespace + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo', + 'any', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo, bar', + 'comma', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + 'Foo [[bar]]', + 'link', + false + ), + + // not a content namespace, different model + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo', + 'any', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo, bar', + 'comma', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + 'Foo [[bar]]', + 'link', + false + ), + ); + } + + + /** + * @dataProvider dataIsCountable + */ + public function testIsCountable( $title, $text, $mode, $expected ) { + global $wgArticleCountMethod; + + $old = $wgArticleCountMethod; + $wgArticleCountMethod = $mode; + + $page = $this->createPage( $title, $text ); + $editInfo = $page->prepareTextForEdit( $page->getText() ); + + $v = $page->isCountable(); + $w = $page->isCountable( $editInfo ); + $wgArticleCountMethod = $old; + + $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + + $this->assertEquals( $expected, $w, "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + } + + public function dataGetParserOutput() { + return array( + array("hello ''world''\n", "<p>hello <i>world</i></p>"), + // @todo: more...? + ); + } + + /** + * @dataProvider dataGetParserOutput + */ + public function testGetParserOutput( $text, $expectedHtml ) { + $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text ); + + $opt = new ParserOptions(); + $po = $page->getParserOutput( $opt ); + $text = $po->getText(); + + $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments + $text = preg_replace( '!\s*(</p>)!sm', '\1', $text ); # don't let tidy confuse us + + $this->assertEquals( $expectedHtml, $text ); + return $po; + } + + static $sections = + + "Intro + +== stuff == +hello world + +== test == +just a test + +== foo == +more stuff +"; + + + public function dataReplaceSection() { + return array( + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "", + "No more", + null, + "No more" + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikiPageTest::$sections ) ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "8", + "No more", + null, + trim( WikiPageTest::$sections ) + ), + array( 'WikiPageTest_testReplaceSection', + WikiPageTest::$sections, + "new", + "No more", + "New", + trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more" + ), + ); + } + + /** + * @dataProvider dataReplaceSection + */ + public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) { + $page = $this->createPage( $title, $text ); + $text = $page->replaceSection( $section, $with, $sectionTitle ); + $text = trim( $text ); + + $this->assertEquals( $expected, $text ); + } + + /* @todo FIXME: fix this! + public function testGetUndoText() { + global $wgDiff3; + + wfSuppressWarnings(); + $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); + wfRestoreWarnings(); + + if( !$haveDiff3 ) { + $this->markTestSkipped( "diff3 not installed or not found" ); + return; + } + + $text = "one"; + $page = $this->createPage( "WikiPageTest_testGetUndoText", $text ); + $rev1 = $page->getRevision(); + + $text .= "\n\ntwo"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two"); + $rev2 = $page->getRevision(); + + $text .= "\n\nthree"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three"); + $rev3 = $page->getRevision(); + + $text .= "\n\nfour"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section four"); + $rev4 = $page->getRevision(); + + $text .= "\n\nfive"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section five"); + $rev5 = $page->getRevision(); + + $text .= "\n\nsix"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section six"); + $rev6 = $page->getRevision(); + + $undo6 = $page->getUndoText( $rev6 ); + if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" ); + $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 ); + + $undo3 = $page->getUndoText( $rev4, $rev2 ); + if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" ); + $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 ); + + $undo2 = $page->getUndoText( $rev2 ); + if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" ); + $this->assertEquals( "one\n\nfive", $undo2 ); + } + */ + + /** + * @todo FIXME: this is a better rollback test than the one below, but it keeps failing in jenkins for some reason. + */ + public function broken_testDoRollback() { + $admin = new User(); + $admin->setName("Admin"); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section two", 0, false, $user1 ); + + $user2 = new User(); + $user2->setName( "127.0.2.13" ); + $text .= "\n\nthree"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section three", 0, false, $user2 ); + + # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing + # or not committed under some circumstances. so, make sure the last revision has the right user name. + $dbr = wfGetDB( DB_SLAVE ); + $this->assertEquals( 3, Revision::countByPageId( $dbr, $page->getId() ) ); + + $page = new WikiPage( $page->getTitle() ); + $rev3 = $page->getRevision(); + $this->assertEquals( '127.0.2.13', $rev3->getUserText() ); + + $rev2 = $rev3->getPrevious(); + $this->assertEquals( '127.0.1.11', $rev2->getUserText() ); + + $rev1 = $rev2->getPrevious(); + $this->assertEquals( 'Admin', $rev1->getUserText() ); + + # now, try the actual rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user2->getName() ), null ); + $errors = $page->doRollback( $user2->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); + $this->assertEquals( "one\n\ntwo", $page->getText() ); + } + + /** + * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason. + */ + public function testDoRollback() { + $admin = new User(); + $admin->setName("Admin"); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + $rev1 = $page->getRevision(); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEdit( $text, "adding section two", 0, false, $user1 ); + + # now, try the rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user1->getName() ), null ); + $errors = $page->doRollback( $user1->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); + $this->assertEquals( "one", $page->getText() ); + } + + public function dataGetAutosummary( ) { + return array( + array( + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ), + + array( + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ), + + array( + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ), + + array( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut + labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ), + + array( + 'foo', + 'bar', + 0, + '/^$/' + ), + ); + } + + /** + * @dataProvider dataGetAutoSummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + $page = $this->newPage( "WikiPageTest_testGetAutosummary" ); + + $summary = $page->getAutosummary( $old, $new, $flags ); + + $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" ); + } + + public function dataGetAutoDeleteReason( ) { + return array( + array( + array(), + false, + false + ), + + array( + array( + array( "first edit", null ), + ), + "/first edit.*only contributor/", + false + ), + + array( + array( + array( "first edit", null ), + array( "second edit", null ), + ), + "/second edit.*only contributor/", + true + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "second edit", "127.0.3.33" ), + ), + "/second edit/", + true + ), + + array( + array( + array( "first edit: " + . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. " + . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea " + . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ), + ), + '/first edit:.*\.\.\."/', + false + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "", "127.0.3.33" ), + ), + "/before blanking.*first edit/", + true + ), + + ); + } + + /** + * @dataProvider dataGetAutoDeleteReason + */ + public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) { + global $wgUser; + + $page = $this->newPage( "WikiPageTest_testGetAutoDeleteReason" ); + + $c = 1; + + foreach ( $edits as $edit ) { + $user = new User(); + + if ( !empty( $edit[1] ) ) $user->setName( $edit[1] ); + else $user = $wgUser; + + $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user ); + + $c += 1; + } + + $reason = $page->getAutoDeleteReason( $hasHistory ); + + if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) $this->assertEquals( $expectedResult, $reason ); + else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), "Autosummary didn't match expected pattern $expectedResult: $reason" ); + + $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) ); + + $page->doDeleteArticle( "done" ); + } + + public 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 <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + $this->hideDeprecated( 'WikiPage::preSaveTransform' ); + $user = new User(); + $user->setName("127.0.0.1"); + + $page = $this->newPage( "WikiPageTest_testPreloadTransform" ); + $text = $page->preSaveTransform( $text, $user ); + + $this->assertEquals( $expected, $text ); + } + +} + diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php index 1d9361f2..93ed3dc7 100644 --- a/tests/phpunit/includes/XmlTest.php +++ b/tests/phpunit/includes/XmlTest.php @@ -193,52 +193,6 @@ class XmlTest extends MediaWikiTestCase { ); } - function testNamespaceSelector() { - $this->assertEquals( - '<select class="namespaceselector" id="namespace" name="namespace">' . "\n" . -'<option value="0">(Main)</option>' . "\n" . -'<option value="1">Talk</option>' . "\n" . -'<option value="2">User</option>' . "\n" . -'<option value="3">User talk</option>' . "\n" . -'<option value="4">MyWiki</option>' . "\n" . -'<option value="5">MyWiki Talk</option>' . "\n" . -'<option value="6">File</option>' . "\n" . -'<option value="7">File talk</option>' . "\n" . -'<option value="8">MediaWiki</option>' . "\n" . -'<option value="9">MediaWiki talk</option>' . "\n" . -'<option value="10">Template</option>' . "\n" . -'<option value="11">Template talk</option>' . "\n" . -'<option value="100">Custom</option>' . "\n" . -'<option value="101">Custom talk</option>' . "\n" . -'</select>', - Xml::namespaceSelector(), - 'Basic namespace selector without custom options' - ); - $this->assertEquals( - '<label for="namespace">Select a namespace:</label>' . -' <select class="namespaceselector" id="namespace" name="myname">' . "\n" . -'<option value="all">all</option>' . "\n" . -'<option value="0">(Main)</option>' . "\n" . -'<option value="1">Talk</option>' . "\n" . -'<option value="2" selected="">User</option>' . "\n" . -'<option value="3">User talk</option>' . "\n" . -'<option value="4">MyWiki</option>' . "\n" . -'<option value="5">MyWiki Talk</option>' . "\n" . -'<option value="6">File</option>' . "\n" . -'<option value="7">File talk</option>' . "\n" . -'<option value="8">MediaWiki</option>' . "\n" . -'<option value="9">MediaWiki talk</option>' . "\n" . -'<option value="10">Template</option>' . "\n" . -'<option value="11">Template talk</option>' . "\n" . -'<option value="100">Custom</option>' . "\n" . -'<option value="101">Custom talk</option>' . "\n" . -'</select>', - Xml::namespaceSelector( $selected = '2', $all = 'all', $element_name = 'myname', $label = 'Select a namespace:' ), - 'Basic namespace selector with custom values' - ); - } - - # # textarea # @@ -297,6 +251,15 @@ class XmlTest extends MediaWikiTestCase { ); } + function testLanguageSelector() { + $select = Xml::languageSelector( 'en', true, null, + array( 'id' => 'testlang' ), wfMessage( 'yourlanguage' ) ); + $this->assertEquals( + '<label for="testlang">Language:</label>', + $select[0] + ); + } + # # JS # diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php index f7ca59e2..d90a6950 100644 --- a/tests/phpunit/includes/ZipDirectoryReaderTest.php +++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php @@ -4,7 +4,7 @@ class ZipDirectoryReaderTest extends MediaWikiTestCase { var $zipDir, $entries; function setUp() { - $this->zipDir = dirname( __FILE__ ) . '/../data/zip'; + $this->zipDir = __DIR__ . '/../data/zip'; } function zipCallback( $entry ) { diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php index b95d8214..5dfceee8 100644 --- a/tests/phpunit/includes/api/ApiBlockTest.php +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiBlockTest extends ApiTestCase { @@ -32,8 +33,6 @@ class ApiBlockTest extends ApiTestCase { * Root cause is https://gerrit.wikimedia.org/r/3434 * Which made the Block/Unblock API to actually verify the token * previously always considered valid (bug 34212). - * - * @group Broken */ function testMakeNormalBlock() { @@ -57,7 +56,7 @@ class ApiBlockTest extends ApiTestCase { 'action' => 'block', 'user' => 'UTApiBlockee', 'reason' => 'Some reason', - 'token' => $pageinfo['blocktoken'] ), $data, false, self::$users['sysop']->user ); + 'token' => $pageinfo['blocktoken'] ), null, false, self::$users['sysop']->user ); $block = Block::newFromTarget('UTApiBlockee'); @@ -69,4 +68,50 @@ class ApiBlockTest extends ApiTestCase { } + /** + * @dataProvider provideBlockUnblockAction + */ + function testGetTokenUsingABlockingAction( $action ) { + $data = $this->doApiRequest( + array( + 'action' => $action, + 'user' => 'UTApiBlockee', + 'gettoken' => '' ), + null, + false, + self::$users['sysop']->user + ); + $this->assertEquals( 34, strlen( $data[0][$action]["{$action}token"] ) ); + } + + /** + * Attempting to block without a token should give a UsageException with + * error message: + * "The token parameter must be set" + * + * @dataProvider provideBlockUnblockAction + * @expectedException UsageException + */ + function testBlockingActionWithNoToken( $action ) { + $this->doApiRequest( + array( + 'action' => $action, + 'user' => 'UTApiBlockee', + 'reason' => 'Some reason', + ), + null, + false, + self::$users['sysop']->user + ); + } + + /** + * Just provide the 'block' and 'unblock' action to test both API calls + */ + function provideBlockUnblockAction() { + return array( + array( 'block' ), + array( 'unblock' ), + ); + } } diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php new file mode 100644 index 00000000..5297d6da --- /dev/null +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -0,0 +1,84 @@ +<?php + +/** + * Tests for MediaWiki api.php?action=edit. + * + * @author Daniel Kinzler + * + * @group API + * @group Database + */ +class ApiEditPageTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function testEdit( ) { + $name = 'ApiEditPageTest_testEdit'; + + // -- test new page -------------------------------------------- + $apiResult = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', ) ); + $apiResult = $apiResult[0]; + + # Validate API result data + $this->assertArrayHasKey( 'edit', $apiResult ); + $this->assertArrayHasKey( 'result', $apiResult['edit'] ); + $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + + $this->assertArrayHasKey( 'new', $apiResult['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); + + $this->assertArrayHasKey( 'pageid', $apiResult['edit'] ); + + // -- test existing page, no change ---------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayHasKey( 'nochange', $data[0]['edit'] ); + + // -- test existing page, with change -------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'different text' ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] ); + + $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] ); + $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] ); + $this->assertNotEquals( + $data[0]['edit']['newrevid'], + $data[0]['edit']['oldrevid'], + "revision id should change after edit" + ); + } + + function testEditAppend() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testEditSection() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testUndo() { + $this->markTestIncomplete( "not yet implemented" ); + } + + function testEditNonText() { + $this->markTestIncomplete( "not yet implemented" ); + } +} diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php new file mode 100644 index 00000000..5243fca1 --- /dev/null +++ b/tests/phpunit/includes/api/ApiOptionsTest.php @@ -0,0 +1,276 @@ +<?php + +/** + * @group API + * @group Database + */ +class ApiOptionsTest extends MediaWikiLangTestCase { + + private $mTested, $mApiMainMock, $mUserMock, $mContext, $mSession; + + private $mOldGetPreferencesHooks = false; + + private static $Success = array( 'options' => 'success' ); + + function setUp() { + parent::setUp(); + + $this->mUserMock = $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mApiMainMock = $this->getMockBuilder( 'ApiBase' ) + ->disableOriginalConstructor() + ->getMock(); + + // Set up groups + $this->mUserMock->expects( $this->any() ) + ->method( 'getEffectiveGroups' )->will( $this->returnValue( array( '*', 'user')) ); + + // Create a new context + $this->mContext = new DerivativeContext( new RequestContext() ); + $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) ); + $this->mContext->setUser( $this->mUserMock ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getContext' ) + ->will( $this->returnValue( $this->mContext ) ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getResult' ) + ->will( $this->returnValue( new ApiResult( $this->mApiMainMock ) ) ); + + + // Empty session + $this->mSession = array(); + + $this->mTested = new ApiOptions( $this->mApiMainMock, 'options' ); + + global $wgHooks; + if ( !isset( $wgHooks['GetPreferences'] ) ) { + $wgHooks['GetPreferences'] = array(); + } + $this->mOldGetPreferencesHooks = $wgHooks['GetPreferences']; + $wgHooks['GetPreferences'][] = array( $this, 'hookGetPreferences' ); + } + + public function tearDown() { + global $wgHooks; + + if ( $this->mOldGetPreferencesHooks !== false ) { + $wgHooks['GetPreferences'] = $this->mOldGetPreferencesHooks; + $this->mOldGetPreferencesHooks = false; + } + + parent::tearDown(); + } + + public function hookGetPreferences( $user, &$preferences ) { + foreach ( array( 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ) as $k ) { + $preferences[$k] = array( + 'type' => 'text', + 'section' => 'test', + 'label' => ' ', + ); + } + + return true; + } + + private function getSampleRequest( $custom = array() ) { + $request = array( + 'token' => '123ABC', + 'change' => null, + 'optionname' => null, + 'optionvalue' => null, + ); + return array_merge( $request, $custom ); + } + + private function executeQuery( $request ) { + $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) ); + $this->mTested->execute(); + return $this->mTested->getResult()->getData(); + } + + /** + * @expectedException UsageException + */ + public function testNoToken() { + $request = $this->getSampleRequest( array( 'token' => null ) ); + + $this->executeQuery( $request ); + } + + public function testAnon() { + $this->mUserMock->expects( $this->once() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( true ) ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'notloggedin', $e->getCodeString() ); + $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoOptionname() { + try { + $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) ); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nooptionname', $e->getCodeString() ); + $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoChanges() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'saveSettings' ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nochanges', $e->getCodeString() ); + $this->assertEquals( 'No changes were requested', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testReset() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'reset' => '' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionWithValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionResetValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name' ) ); + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testChange() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 1 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeNull' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 6 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testResetChangeOption() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $args = array( + 'reset' => '', + 'change' => 'willBeHappy=Happy', + 'optionname' => 'name', + 'optionvalue' => 'value' + ); + + $response = $this->executeQuery( $this->getSampleRequest( $args ) ); + + $this->assertEquals( self::$Success, $response ); + } +} diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php index 70c20746..2566c6cd 100644 --- a/tests/phpunit/includes/api/ApiPurgeTest.php +++ b/tests/phpunit/includes/api/ApiPurgeTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiPurgeTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiQueryTest.php b/tests/phpunit/includes/api/ApiQueryTest.php index ae05a30a..a4b9dc70 100644 --- a/tests/phpunit/includes/api/ApiQueryTest.php +++ b/tests/phpunit/includes/api/ApiQueryTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiQueryTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php index 1d9c3238..c3eacd5b 100644 --- a/tests/phpunit/includes/api/ApiTest.php +++ b/tests/phpunit/includes/api/ApiTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ class ApiTest extends ApiTestCase { diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 8801391f..b84292e3 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -1,10 +1,6 @@ <?php abstract class ApiTestCase extends MediaWikiLangTestCase { - /** - * @var Array of ApiTestUser - */ - public static $users; protected static $apiUrl; /** @@ -23,13 +19,13 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { $wgRequest = new FauxRequest( array() ); self::$users = array( - 'sysop' => new ApiTestUser( + 'sysop' => new TestUser( 'Apitestsysop', 'Api Test Sysop', 'api_test_sysop@example.com', array( 'sysop' ) ), - 'uploader' => new ApiTestUser( + 'uploader' => new TestUser( 'Apitestuser', 'Api Test User', 'api_test_user@example.com', @@ -43,15 +39,31 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { } - protected function doApiRequest( $params, $session = null, $appendModule = false, $user = null ) { + protected function doApiRequest( Array $params, Array $session = null, $appendModule = false, User $user = null ) { + global $wgRequest, $wgUser; + if ( is_null( $session ) ) { - $session = array(); + # re-use existing global session by default + $session = $wgRequest->getSessionArray(); } - $context = $this->apiContext->newTestContext( $params, $session, $user ); + # set up global environment + if ( $user ) { + $wgUser = $user; + } + + $wgRequest = new FauxRequest( $params, true, $session ); + RequestContext::getMain()->setRequest( $wgRequest ); + + # set up local environment + $context = $this->apiContext->newTestContext( $wgRequest, $wgUser ); + $module = new ApiMain( $context, true ); + + # run it! $module->execute(); + # construct result $results = array( $module->getResultData(), $context->getRequest(), @@ -68,11 +80,17 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { * Add an edit token to the API request * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the * request, without actually requesting a "real" edit token - * @param $params: key-value API params - * @param $session: session array - * @param $user String|null A User object for the context + * @param $params Array: key-value API params + * @param $session Array|null: session array + * @param $user User|null A User object for the context */ - protected function doApiRequestWithToken( $params, $session, $user = null ) { + protected function doApiRequestWithToken( Array $params, Array $session = null, User $user = null ) { + global $wgRequest; + + if ( $session === null ) { + $session = $wgRequest->getSessionArray(); + } + if ( $session['wsToken'] ) { // add edit token to fake session $session['wsEditToken'] = $session['wsToken']; @@ -97,17 +115,17 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { 'lgtoken' => $token, 'lgname' => self::$users['sysop']->username, 'lgpassword' => self::$users['sysop']->password - ), $data ); + ), $data[2] ); return $data; } - protected function getTokenList( $user ) { + protected function getTokenList( $user, $session = null ) { $data = $this->doApiRequest( array( 'action' => 'query', 'titles' => 'Main Page', - 'intoken' => 'edit|delete|protect|move|block|unblock', - 'prop' => 'info' ), false, $user->user ); + 'intoken' => 'edit|delete|protect|move|block|unblock|watch', + 'prop' => 'info' ), $session, false, $user->user ); return $data; } } @@ -154,14 +172,13 @@ class ApiTestContext extends RequestContext { /** * Returns a DerivativeContext with the request variables in place * - * @param $params Array key-value API params - * @param $session Array session data + * @param $request WebRequest request object including parameters and session * @param $user User or null * @return DerivativeContext */ - public function newTestContext( $params, $session, $user = null ) { + public function newTestContext( WebRequest $request, User $user = null ) { $context = new DerivativeContext( $this ); - $context->setRequest( new FauxRequest( $params, true, $session ) ); + $context->setRequest( $request ); if ( $user !== null ) { $context->setUser( $user ); } diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php index 7a700326..642fed05 100644 --- a/tests/phpunit/includes/api/ApiUploadTest.php +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -1,6 +1,7 @@ <?php /** + * @group API * @group Database */ diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php index b7803746..d2e98152 100644 --- a/tests/phpunit/includes/api/ApiWatchTest.php +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -1,8 +1,9 @@ <?php /** + * @group API * @group Database - * @todo This test suite is severly broken and need a full review + * @todo This test suite is severly broken and need a full review */ class ApiWatchTest extends ApiTestCase { @@ -10,28 +11,28 @@ class ApiWatchTest extends ApiTestCase { parent::setUp(); $this->doLogin(); } - + function getTokens() { - return $this->getTokenList( self::$users['sysop'] ); + $data = $this->getTokenList( self::$users['sysop'] ); + + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + return $pageinfo; } /** - * @group Broken */ function testWatchEdit() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'edit', 'title' => 'UTPage', 'text' => 'new text', 'token' => $pageinfo['edittoken'], - 'watchlist' => 'watch' ), $data ); + 'watchlist' => 'watch' ) ); $this->assertArrayHasKey( 'edit', $data[0] ); $this->assertArrayHasKey( 'result', $data[0]['edit'] ); $this->assertEquals( 'Success', $data[0]['edit']['result'] ); @@ -41,13 +42,14 @@ class ApiWatchTest extends ApiTestCase { /** * @depends testWatchEdit - * @group Broken */ function testWatchClear() { - + + $pageinfo = $this->getTokens(); + $data = $this->doApiRequest( array( 'action' => 'query', - 'list' => 'watchlist' ), $data ); + 'list' => 'watchlist' ) ); if ( isset( $data[0]['query']['watchlist'] ) ) { $wl = $data[0]['query']['watchlist']; @@ -56,7 +58,8 @@ class ApiWatchTest extends ApiTestCase { $data = $this->doApiRequest( array( 'action' => 'watch', 'title' => $page['title'], - 'unwatch' => true ), $data ); + 'unwatch' => true, + 'token' => $pageinfo['watchtoken'] ) ); } } $data = $this->doApiRequest( array( @@ -70,22 +73,17 @@ class ApiWatchTest extends ApiTestCase { } /** - * @group Broken - */ + */ function testWatchProtect() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'protect', 'token' => $pageinfo['protecttoken'], 'title' => 'UTPage', 'protections' => 'edit=sysop', - 'watchlist' => 'unwatch' ), $data ); + 'watchlist' => 'unwatch' ) ); $this->assertArrayHasKey( 'protect', $data[0] ); $this->assertArrayHasKey( 'protections', $data[0]['protect'] ); @@ -94,21 +92,20 @@ class ApiWatchTest extends ApiTestCase { } /** - * @group Broken */ function testGetRollbackToken() { - - $data = $this->getTokens(); - + + $pageinfo = $this->getTokens(); + if ( !Title::newFromText( 'UTPage' )->exists() ) { - $this->markTestIncomplete( "The article [[UTPage]] does not exist" ); + $this->markTestSkipped( "The article [[UTPage]] does not exist" ); //TODO: just create it? } $data = $this->doApiRequest( array( 'action' => 'query', 'prop' => 'revisions', 'titles' => 'UTPage', - 'rvtoken' => 'rollback' ), $data ); + 'rvtoken' => 'rollback' ) ); $this->assertArrayHasKey( 'query', $data[0] ); $this->assertArrayHasKey( 'pages', $data[0]['query'] ); @@ -116,7 +113,7 @@ class ApiWatchTest extends ApiTestCase { $key = array_pop( $keys ); if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) { - $this->markTestIncomplete( "Target page (UTPage) doesn't exist" ); + $this->markTestSkipped( "Target page (UTPage) doesn't exist" ); } $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] ); @@ -128,21 +125,27 @@ class ApiWatchTest extends ApiTestCase { } /** - * @depends testGetRollbackToken * @group Broken + * Broken because there is currently no revision info in the $pageinfo + * + * @depends testGetRollbackToken */ function testWatchRollback( $data ) { $keys = array_keys( $data[0]['query']['pages'] ); $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]['revisions'][0]; + $pageinfo = $data[0]['query']['pages'][$key]; + $revinfo = $pageinfo['revisions'][0]; try { $data = $this->doApiRequest( array( 'action' => 'rollback', 'title' => 'UTPage', - 'user' => $pageinfo['user'], + 'user' => $revinfo['user'], 'token' => $pageinfo['rollbacktoken'], - 'watchlist' => 'watch' ), $data ); + 'watchlist' => 'watch' ) ); + + $this->assertArrayHasKey( 'rollback', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); } catch( UsageException $ue ) { if( $ue->getCodeString() == 'onlyauthor' ) { $this->markTestIncomplete( "Only one author to 'UTPage', cannot test rollback" ); @@ -150,32 +153,23 @@ class ApiWatchTest extends ApiTestCase { $this->fail( "Received error '" . $ue->getCodeString() . "'" ); } } - - $this->assertArrayHasKey( 'rollback', $data[0] ); - $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); } /** - * @group Broken */ function testWatchDelete() { - - $data = $this->getTokens(); - - $keys = array_keys( $data[0]['query']['pages'] ); - $key = array_pop( $keys ); - $pageinfo = $data[0]['query']['pages'][$key]; + $pageinfo = $this->getTokens(); $data = $this->doApiRequest( array( 'action' => 'delete', 'token' => $pageinfo['deletetoken'], - 'title' => 'UTPage' ), $data ); + 'title' => 'UTPage' ) ); $this->assertArrayHasKey( 'delete', $data[0] ); $this->assertArrayHasKey( 'title', $data[0]['delete'] ); $data = $this->doApiRequest( array( 'action' => 'query', - 'list' => 'watchlist' ), $data ); + 'list' => 'watchlist' ) ); $this->markTestIncomplete( 'This test needs to verify the deleted article was added to the users watchlist' ); } diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php new file mode 100644 index 00000000..69b01ea7 --- /dev/null +++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php @@ -0,0 +1,24 @@ +<?php + +/** + * Checks that all API query modules, core and extensions, have unique prefixes + * @group API + */ +class PrefixUniquenessTest extends MediaWikiTestCase { + public function testPrefixes() { + $main = new ApiMain( new FauxRequest() ); + $query = new ApiQuery( $main, 'foo', 'bar' ); + $modules = $query->getModules(); + $prefixes = array(); + + foreach ( $modules as $name => $class ) { + $module = new $class( $main, $name ); + $prefix = $module->getModulePrefix(); + if ( isset( $prefixes[$prefix] ) ) { + $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" ); + } + $prefixes[$module->getModulePrefix()] = $class; + } + $this->assertTrue( true ); // dummy call to make this test non-incomplete + } +} diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php index 86c0a828..8b6a3849 100644 --- a/tests/phpunit/includes/api/RandomImageGenerator.php +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -79,7 +79,7 @@ class RandomImageGenerator { foreach ( array( '/usr/share/dict/words', '/usr/dict/words', - dirname( __FILE__ ) . '/words.txt' ) + __DIR__ . '/words.txt' ) as $dictionaryFile ) { if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { $this->dictionaryFile = $dictionaryFile; diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php index f3a14e5b..ee345623 100644 --- a/tests/phpunit/includes/api/generateRandomImages.php +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -6,14 +6,18 @@ */ // Evaluate the include path relative to this file -$IP = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ); +$IP = dirname( dirname( dirname( dirname( __DIR__ ) ) ) ); // Start up MediaWiki in command-line mode require_once( "$IP/maintenance/Maintenance.php" ); -require("RandomImageGenerator.php"); +require( __DIR__ . "/RandomImageGenerator.php" ); class GenerateRandomImages extends Maintenance { + public function getDbType() { + return Maintenance::DB_NONE; + } + public function execute() { $getOptSpec = array( diff --git a/tests/phpunit/includes/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php new file mode 100644 index 00000000..a8b987e2 --- /dev/null +++ b/tests/phpunit/includes/cache/GenderCacheTest.php @@ -0,0 +1,101 @@ +<?php + +/** + * @group Database + * @group Cache + */ +class GenderCacheTest extends MediaWikiLangTestCase { + + function setUp() { + global $wgDefaultUserOptions; + parent::setUp(); + //ensure the correct default gender + $wgDefaultUserOptions['gender'] = 'unknown'; + } + + function addDBData() { + $user = User::newFromName( 'UTMale' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTMalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'male' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTFemale' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTFemalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'female' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTDefaultGender' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTDefaultGenderPassword' ); + } + //ensure the default gender + $user->setOption( 'gender', null ); + $user->saveSettings(); + } + + /** + * test usernames + * + * @dataProvider dataUserName + */ + function testUserName( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $username ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + /** + * genderCache should work with user objects, too + * + * @dataProvider dataUserName + */ + function testUserObjects( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $user = User::newFromName( $username ); + $gender = $genderCache->getGenderOf( $user ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + function dataUserName() { + return array( + array( 'UTMale', 'male' ), + array( 'UTFemale', 'female' ), + array( 'UTDefaultGender', 'unknown' ), + array( 'UTNotExist', 'unknown' ), + //some not valid user + array( '127.0.0.1', 'unknown' ), + array( 'user@test', 'unknown' ), + ); + } + + /** + * test strip of subpages to avoid unnecessary queries + * against the never existing username + * + * @dataProvider dataStripSubpages + */ + function testStripSubpages( $pageWithSubpage, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $pageWithSubpage ); + $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" ); + } + + function dataStripSubpages() { + return array( + array( 'UTMale/subpage', 'male' ), + array( 'UTFemale/subpage', 'female' ), + array( 'UTDefaultGender/subpage', 'unknown' ), + array( 'UTNotExist/subpage', 'unknown' ), + array( '127.0.0.1/subpage', 'unknown' ), + ); + } +} diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php new file mode 100644 index 00000000..30bfb124 --- /dev/null +++ b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php @@ -0,0 +1,239 @@ +<?php + +/** + * Test for ProcessCacheLRU class. + * + * Note that it uses the ProcessCacheLRUTestable class which extends some + * properties and methods visibility. That class is defined at the end of the + * file containing this class. + * + * @group Cache + */ +class ProcessCacheLRUTest extends MediaWikiTestCase { + + /** + * Helper to verify emptiness of a cache object. + * Compare against an array so we get the cache content difference. + */ + function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) { + $this->assertAttributeEquals( array(), 'cache', $cache, $msg ); + } + + /** + * Helper to fill a cache object passed by reference + */ + function fillCache( &$cache, $numEntries ) { + // Fill cache with three values + for( $i=1; $i<=$numEntries; $i++) { + $cache->set( "cache-key-$i", "prop-$i", "value-$i" ); + } + } + + /** + * Generates an array of what would be expected in cache for a given cache + * size and a number of entries filled in sequentially + */ + function getExpectedCache( $cacheMaxEntries, $entryToFill ) { + $expected = array(); + + if( $entryToFill === 0 ) { + # The cache is empty! + return array(); + } elseif( $entryToFill <= $cacheMaxEntries ) { + # Cache is not fully filled + $firstKey = 1; + } else { + # Cache overflowed + $firstKey = 1 + $entryToFill - $cacheMaxEntries; + } + + $lastKey = $entryToFill; + + for( $i=$firstKey; $i<=$lastKey; $i++ ) { + $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" ); + } + return $expected; + } + + /** + * Highlight diff between assertEquals and assertNotSame + */ + function testPhpUnitArrayEquality() { + $one = array( 'A' => 1, 'B' => 2 ); + $two = array( 'B' => 2, 'A' => 1 ); + $this->assertEquals( $one, $two ); // == + $this->assertNotSame( $one, $two ); // === + } + + /** + * @dataProvider provideInvalidConstructorArg + * @expectedException MWException + */ + function testConstructorGivenInvalidValue( $maxSize ) { + $c = new ProcessCacheLRUTestable( $maxSize ); + } + + /** + * Value which are forbidden by the constructor + */ + function provideInvalidConstructorArg() { + return array( + array( null ), + array( array() ), + array( new stdClass() ), + array( 0 ), + array( '5' ), + array( -1 ), + ); + } + + function testAddAndGetAKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + // First set just one value + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $this->assertEquals( 1, $oneCache->getEntriesCount() ); + $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) ); + $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + function testDeleteOldKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $oneCache->set( 'cache-key', 'prop1', 'value2' ); + $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + /** + * This test that we properly overflow when filling a cache with + * a sequence of always different cache-keys. Meant to verify we correclty + * delete the older key. + * + * @dataProvider provideCacheFilling + * @param $cacheMaxEntries Maximum entry the created cache will hold + * @param $entryToFill Number of entries to insert in the created cache. + */ + function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) { + $cache = new ProcessCacheLRUTestable( $cacheMaxEntries ); + $this->fillCache( $cache, $entryToFill); + + $this->assertSame( + $this->getExpectedCache( $cacheMaxEntries, $entryToFill ), + $cache->getCache(), + "Filling a $cacheMaxEntries entries cache with $entryToFill entries" + ); + + } + + /** + * Provider for testFillingCache + */ + function provideCacheFilling() { + // ($cacheMaxEntries, $entryToFill, $msg='') + return array( + array( 1, 0 ), + array( 1, 1 ), + array( 1, 2 ), # overflow + array( 5, 33 ), # overflow + ); + + } + + /** + * Create a cache with only one remaining entry then update + * the first inserted entry. Should bump it to the top. + */ + function testReplaceExistingKeyShouldBumpEntryToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + // Fill cache leaving just one remaining slot + $this->fillCache( $cache, $maxEntries - 1 ); + + // Set an existing cache key + $cache->set( "cache-key-1", "prop-1", "new-value-for-1" ); + + $this->assertSame( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ), + ), + $cache->getCache() + ); + } + + function testRecentlyAccessedKeyStickIn() { + $cache = new ProcessCacheLRUTestable( 2 ); + $cache->set( 'first' , 'prop1', 'value1' ); + $cache->set( 'second', 'prop2', 'value2' ); + + // Get first + $cache->get( 'first', 'prop1' ); + // Cache a third value, should invalidate the least used one + $cache->set( 'third', 'prop3', 'value3' ); + + $this->assertFalse( $cache->has( 'second', 'prop2' ) ); + } + + /** + * This first create a full cache then update the value for the 2nd + * filled entry. + * Given a cache having 1,2,3 as key, updating 2 should bump 2 to + * the top of the queue with the new value: 1,3,2* (* = updated). + */ + function testReplaceExistingKeyInAFullCacheShouldBumpToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + $this->fillCache( $cache, $maxEntries ); + + // Set an existing cache key + $cache->set( "cache-key-2", "prop-2", "new-value-for-2" ); + $this->assertSame( + array( + 'cache-key-1' => array( 'prop-1' => 'value-1' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ), + ), + $cache->getCache() + ); + $this->assertEquals( 'new-value-for-2', + $cache->get( 'cache-key-2', 'prop-2' ) + ); + } + + function testBumpExistingKeyToTop() { + $cache = new ProcessCacheLRUTestable( 3 ); + $this->fillCache( $cache, 3 ); + + // Set the very first cache key to a new value + $cache->set( "cache-key-1", "prop-1", "new value for 1" ); + $this->assertEquals( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-1' => array( 'prop-1' => 'new value for 1' ), + ), + $cache->getCache() + ); + + } + +} + +/** + * Overrides some ProcessCacheLRU methods and properties accessibility. + */ +class ProcessCacheLRUTestable extends ProcessCacheLRU { + public $cache = array(); + + public function getCache() { + return $this->cache; + } + public function getEntriesCount() { + return count( $this->cache ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php new file mode 100644 index 00000000..e37cd445 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSQLTest.php @@ -0,0 +1,147 @@ +<?php + +/** + * Test the abstract database layer + * Using Mysql for the sql at the moment TODO + * + * @group Database + */ +class DatabaseSQLTest extends MediaWikiTestCase { + + public function setUp() { + // TODO support other DBMS or find another way to do it + if( $this->db->getType() !== 'mysql' ) { + $this->markTestSkipped( 'No mysql database' ); + } + } + + /** + * @dataProvider dataSelectSQLText + */ + function testSelectSQLText( $sql, $sqlText ) { + $this->assertEquals( trim( $this->db->selectSQLText( + isset( $sql['tables'] ) ? $sql['tables'] : array(), + isset( $sql['fields'] ) ? $sql['fields'] : array(), + isset( $sql['conds'] ) ? $sql['conds'] : array(), + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : array(), + isset( $sql['join_conds'] ) ? $sql['join_conds'] : array() + ) ), $sqlText ); + } + + function dataSelectSQLText() { + return array( + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + ), + "SELECT field,field2 AS alias " . + "FROM `unittest_table` " . + "WHERE alias = 'text'" + ), + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + ), + "SELECT field,field2 AS alias " . + "FROM `unittest_table` " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field HAVING COUNT(*) > 1 " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => array( 'field', 'field2' ), 'HAVING' => array( 'COUNT(*) > 1', 'field' => 1 ) ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + )), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM `unittest_table` LEFT JOIN `unittest_table2` `t2` ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " . + "LIMIT 1" + ), + ); + } + + /** + * @dataProvider dataConditional + */ + function testConditional( $sql, $sqlText ) { + $this->assertEquals( trim( $this->db->conditional( + $sql['conds'], + $sql['true'], + $sql['false'] + ) ), $sqlText ); + } + + function dataConditional() { + return array( + array( + array( + 'conds' => array( 'field' => 'text' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => array( 'field' => 'text', 'field2' => 'anothertext' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => 'field=1', + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field=1 THEN 1 ELSE NULL END)" + ), + ); + } +}
\ No newline at end of file diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php index 067c731a..d226598b 100644 --- a/tests/phpunit/includes/db/DatabaseSqliteTest.php +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -250,6 +250,16 @@ class DatabaseSqliteTest extends MediaWikiTestCase { } } + public function testInsertIdType() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $this->assertInstanceOf( 'ResultWrapper', + $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ ), "Database creationg" ); + $this->assertTrue( $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ ), + "Insertion worked" ); + $this->assertEquals( "integer", gettype( $db->insertId() ), "Actual typecheck" ); + $this->assertTrue( $db->close(), "closing database" ); + } + private function prepareDB( $version ) { static $maint = null; if ( $maint === null ) { diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php new file mode 100644 index 00000000..9dcaf2b3 --- /dev/null +++ b/tests/phpunit/includes/db/ORMRowTest.php @@ -0,0 +1,234 @@ +<?php + +/** + * Abstract class to construct tests for ORMRow deriving classes. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * + * @group ORM + * + * The database group has as a side effect that temporal database tables are created. This makes + * it possible to test without poisoning a production database. + * @group Database + * + * Some of the tests takes more time, and needs therefor longer time before they can be aborted + * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases + * that hold the first tests in a pending state awaiting access to the database. + * @group medium + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class ORMRowTest extends \MediaWikiTestCase { + + /** + * @since 1.20 + * @return string + */ + protected abstract function getRowClass(); + + /** + * @since 1.20 + * @return IORMTable + */ + protected abstract function getTableInstance(); + + /** + * @since 1.20 + * @return array + */ + public abstract function constructorTestProvider(); + + /** + * @since 1.20 + * @param IORMRow $row + * @param array $data + */ + protected function verifyFields( IORMRow $row, array $data ) { + foreach ( array_keys( $data ) as $fieldName ) { + $this->assertEquals( $data[$fieldName], $row->getField( $fieldName ) ); + } + } + + /** + * @since 1.20 + * @param array $data + * @param boolean $loadDefaults + * @return IORMRow + */ + protected function getRowInstance( array $data, $loadDefaults ) { + $class = $this->getRowClass(); + return new $class( $this->getTableInstance(), $data, $loadDefaults ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockValues() { + return array( + 'id' => 1, + 'str' => 'foobar4645645', + 'int' => 42, + 'float' => 4.2, + 'bool' => true, + 'array' => array( 42, 'foobar' ), + 'blob' => new stdClass() + ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockFields() { + $mockValues = $this->getMockValues(); + $mockFields = array(); + + foreach ( $this->getTableInstance()->getFields() as $name => $type ) { + if ( $name !== 'id' ) { + $mockFields[$name] = $mockValues[$type]; + } + } + + return $mockFields; + } + + /** + * @since 1.20 + * @return array of IORMRow + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->constructorTestProvider() as $arguments ) { + $instances[] = array( call_user_func_array( array( $this, 'getRowInstance' ), $arguments ) ); + } + + return $instances; + } + + /** + * @dataProvider constructorTestProvider + */ + public function testConstructor( array $data, $loadDefaults ) { + $this->verifyFields( $this->getRowInstance( $data, $loadDefaults ), $data ); + } + + /** + * @dataProvider constructorTestProvider + */ + public function testSave( array $data, $loadDefaults ) { + $item = $this->getRowInstance( $data, $loadDefaults ); + + $this->assertTrue( $item->save() ); + + $this->assertTrue( $item->hasIdField() ); + $this->assertTrue( is_integer( $item->getId() ) ); + + $id = $item->getId(); + + $this->assertTrue( $item->save() ); + + $this->assertEquals( $id, $item->getId() ); + + $this->verifyFields( $item, $data ); + } + + /** + * @dataProvider constructorTestProvider + */ + public function testRemove( array $data, $loadDefaults ) { + $item = $this->getRowInstance( $data, $loadDefaults ); + + $this->assertTrue( $item->save() ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->assertTrue( $item->save() ); + + $this->verifyFields( $item, $data ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->verifyFields( $item, $data ); + } + + /** + * @dataProvider instanceProvider + */ + public function testSetField( IORMRow $item ) { + foreach ( $this->getMockFields() as $name => $value ) { + $item->setField( $name, $value ); + $this->assertEquals( $value, $item->getField( $name ) ); + } + } + + /** + * @since 1.20 + * @param array $expected + * @param IORMRow $item + */ + protected function assertFieldValues( array $expected, IORMRow $item ) { + foreach ( $expected as $name => $type ) { + if ( $name !== 'id' ) { + $this->assertEquals( $expected[$name], $item->getField( $name ) ); + } + } + } + + /** + * @dataProvider instanceProvider + */ + public function testSetFields( IORMRow $item ) { + $originalValues = $item->getFields(); + + $item->setFields( array(), false ); + + foreach ( $item->getTable()->getFields() as $name => $type ) { + $originalHas = array_key_exists( $name, $originalValues ); + $newHas = $item->hasField( $name ); + + $this->assertEquals( $originalHas, $newHas ); + + if ( $originalHas && $newHas ) { + $this->assertEquals( $originalValues[$name], $item->getField( $name ) ); + } + } + + $mockFields = $this->getMockFields(); + + $item->setFields( $mockFields, false ); + + $this->assertFieldValues( $originalValues, $item ); + + $item->setFields( $mockFields, true ); + + $this->assertFieldValues( $mockFields, $item ); + } + + // TODO: test all of the methods! + +}
\ No newline at end of file diff --git a/tests/phpunit/includes/db/TestORMRowTest.php b/tests/phpunit/includes/db/TestORMRowTest.php new file mode 100644 index 00000000..afd1cb80 --- /dev/null +++ b/tests/phpunit/includes/db/TestORMRowTest.php @@ -0,0 +1,174 @@ +<?php + +/** + * Tests for the TestORMRow class. + * TestORMRow is a dummy class to be able to test the abstract ORMRow class. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * + * @group ORM + * + * The database group has as a side effect that temporal database tables are created. This makes + * it possible to test without poisoning a production database. + * @group Database + * + * Some of the tests takes more time, and needs therefor longer time before they can be aborted + * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases + * that hold the first tests in a pending state awaiting access to the database. + * @group medium + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +require_once __DIR__ . "/ORMRowTest.php"; + +class TestORMRowTest extends ORMRowTest { + + /** + * @since 1.20 + * @return string + */ + protected function getRowClass() { + return 'TestORMRow'; + } + + /** + * @since 1.20 + * @return IORMTable + */ + protected function getTableInstance() { + return TestORMTable::singleton(); + } + + public function setUp() { + parent::setUp(); + + $dbw = wfGetDB( DB_MASTER ); + + $isSqlite = $GLOBALS['wgDBtype'] === 'sqlite'; + + $idField = $isSqlite ? 'INTEGER' : 'INT unsigned'; + $primaryKey = $isSqlite ? 'PRIMARY KEY AUTOINCREMENT' : 'auto_increment PRIMARY KEY'; + + $dbw->query( + 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . '( + test_id ' . $idField . ' NOT NULL ' . $primaryKey . ', + test_name VARCHAR(255) NOT NULL, + test_age TINYINT unsigned NOT NULL, + test_height FLOAT NOT NULL, + test_awesome TINYINT unsigned NOT NULL, + test_stuff BLOB NOT NULL, + test_moarstuff BLOB NOT NULL, + test_time varbinary(14) NOT NULL + );' + ); + } + + public function constructorTestProvider() { + return array( + array( + array( + 'name' => 'Foobar', + 'age' => 42, + 'height' => 9000.1, + 'awesome' => true, + 'stuff' => array( 13, 11, 7, 5, 3, 2 ), + 'moarstuff' => (object)array( 'foo' => 'bar', 'bar' => array( 4, 2 ), 'baz' => true ) + ), + true + ), + ); + } + +} + +class TestORMRow extends ORMRow {} + +class TestORMTable extends ORMTable { + + /** + * Returns the name of the database table objects of this type are stored in. + * + * @since 1.20 + * + * @return string + */ + public function getName() { + return 'orm_test'; + } + + /** + * Returns the name of a IORMRow implementing class that + * represents single rows in this table. + * + * @since 1.20 + * + * @return string + */ + public function getRowClass() { + return 'TestORMRow'; + } + + /** + * Returns an array with the fields and their types this object contains. + * This corresponds directly to the fields in the database, without prefix. + * + * field name => type + * + * Allowed types: + * * id + * * str + * * int + * * float + * * bool + * * array + * * blob + * + * @since 1.20 + * + * @return array + */ + public function getFields() { + return array( + 'id' => 'id', + 'name' => 'str', + 'age' => 'int', + 'height' => 'float', + 'awesome' => 'bool', + 'stuff' => 'array', + 'moarstuff' => 'blob', + 'time' => 'int', // TS_MW + ); + } + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected function getFieldPrefix() { + return 'test_'; + } + + +} diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php index 5a4e66d4..246b2918 100644 --- a/tests/phpunit/includes/debug/MWDebugTest.php +++ b/tests/phpunit/includes/debug/MWDebugTest.php @@ -12,6 +12,11 @@ class MWDebugTest extends MediaWikiTestCase { } /** Clear log before each test */ MWDebug::clearLog(); + wfSuppressWarnings(); + } + + function tearDown() { + wfRestoreWarnings(); } function testAddLog() { @@ -30,7 +35,7 @@ class MWDebugTest extends MediaWikiTestCase { $this->assertEquals( array( array( 'msg' => 'Warning message', 'type' => 'warn', - 'caller' => 'MWDebug::warning', + 'caller' => 'MWDebugTest::testAddWarning', ) ), MWDebug::getLog() ); diff --git a/tests/phpunit/includes/filerepo/FileBackendTest.php b/tests/phpunit/includes/filerepo/FileBackendTest.php index da44797a..a2dc5c6c 100644 --- a/tests/phpunit/includes/filerepo/FileBackendTest.php +++ b/tests/phpunit/includes/filerepo/FileBackendTest.php @@ -3,11 +3,11 @@ /** * @group FileRepo * @group FileBackend + * @group medium */ class FileBackendTest extends MediaWikiTestCase { private $backend, $multiBackend; private $filesToPrune = array(); - private $dirsToPrune = array(); private static $backendToUse; function setUp() { @@ -23,10 +23,14 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $wgFileBackends as $conf ) { if ( $conf['name'] == $name ) { $useConfig = $conf; + break; } } $useConfig['name'] = 'localtesting'; // swap name - $class = $conf['class']; + $useConfig['shardViaHashLevels'] = array( // test sharding + 'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 ) + ); + $class = $useConfig['class']; self::$backendToUse = new $class( $useConfig ); $this->singleBackend = self::$backendToUse; } @@ -34,6 +38,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->singleBackend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'fsLockManager', + #'parallelize' => 'implicit', 'containerPaths' => array( 'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1", 'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" ) @@ -42,6 +47,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->multiBackend = new FileBackendMultiWrite( array( 'name' => 'localtesting', 'lockManager' => 'fsLockManager', + 'parallelize' => 'implicit', 'backends' => array( array( 'name' => 'localmutlitesting1', @@ -204,7 +210,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - function doTestStore( $op ) { + private function doTestStore( $op ) { $backendName = $this->backendClass(); $source = $op['src']; @@ -219,9 +225,9 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Store from $source to $dest succeeded without warnings ($backendName)." ); - $this->assertEquals( array(), $status->errors, + $this->assertEquals( true, $status->isOK(), "Store from $source to $dest succeeded ($backendName)." ); $this->assertEquals( array( 0 => true ), $status->success, "Store from $source to $dest has proper 'success' field in Status ($backendName)." ); @@ -238,13 +244,15 @@ class FileBackendTest extends MediaWikiTestCase { $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); $this->assertEquals( $props1, $props2, "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $dest ) ); } public function provider_testStore() { $cases = array(); $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath(); - $toPath = $this->baseStorePath() . '/unittest-cont1/fun/obj1.txt'; + $toPath = $this->baseStorePath() . '/unittest-cont1/e/fun/obj1.txt'; $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ); $cases[] = array( $op, // operation @@ -286,7 +294,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - function doTestCopy( $op ) { + private function doTestCopy( $op ) { $backendName = $this->backendClass(); $source = $op['src']; @@ -296,7 +304,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { @@ -305,7 +313,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Copy from $source to $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Copy from $source to $dest succeeded ($backendName)." ); @@ -325,13 +333,15 @@ class FileBackendTest extends MediaWikiTestCase { $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); $this->assertEquals( $props1, $props2, "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); } public function provider_testCopy() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/file.txt'; - $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest ); $cases[] = array( @@ -384,7 +394,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { @@ -392,7 +402,7 @@ class FileBackendTest extends MediaWikiTestCase { } $status = $this->backend->doOperation( $op ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Move from $source to $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Move from $source to $dest succeeded ($backendName)." ); @@ -414,13 +424,15 @@ class FileBackendTest extends MediaWikiTestCase { "Source file does not exist accourding to props ($backendName)." ); $this->assertEquals( true, $props2['fileExists'], "Destination file exists accourding to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); } public function provider_testMove() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/file.txt'; - $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest ); $cases[] = array( @@ -472,13 +484,13 @@ class FileBackendTest extends MediaWikiTestCase { if ( $withSource ) { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); } $status = $this->backend->doOperation( $op ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Deletion of file at $source succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Deletion of file at $source succeeded ($backendName)." ); @@ -499,12 +511,14 @@ class FileBackendTest extends MediaWikiTestCase { $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); $this->assertFalse( $props1['fileExists'], "Source file $source does not exist according to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source ) ); } public function provider_testDelete() { $cases = array(); - $source = $this->baseStorePath() . '/unittest-cont1/myfacefile.txt'; + $source = $this->baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; $op = array( 'op' => 'delete', 'src' => $source ); $cases[] = array( @@ -554,13 +568,13 @@ class FileBackendTest extends MediaWikiTestCase { if ( $alreadyExists ) { $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $dest succeeded ($backendName)." ); } $status = $this->backend->doOperation( $op ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of file at $dest succeeded ($backendName)." ); @@ -590,6 +604,8 @@ class FileBackendTest extends MediaWikiTestCase { $this->backend->getFileSize( array( 'src' => $dest ) ), "Destination file $dest has original size according to props ($backendName)." ); } + + $this->assertBackendPathsConsistent( array( $dest ) ); } /** @@ -598,7 +614,7 @@ class FileBackendTest extends MediaWikiTestCase { public function provider_testCreate() { $cases = array(); - $dest = $this->baseStorePath() . '/unittest-cont2/myspacefile.txt'; + $dest = $this->baseStorePath() . '/unittest-cont2/a/myspacefile.txt'; $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest ); $cases[] = array( @@ -649,6 +665,54 @@ class FileBackendTest extends MediaWikiTestCase { return $cases; } + public function testDoQuickOperations() { + $this->backend = $this->singleBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + } + + private function doTestDoQuickOperations() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $files = array( + "$base/unittest-cont1/e/fileA.a", + "$base/unittest-cont1/e/fileB.a", + "$base/unittest-cont1/e/fileC.a" + ); + $ops = array(); + $purgeOps = array(); + foreach ( $files as $path ) { + $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); + $this->assertGoodStatus( $status, + "Preparing $path succeeded without warnings ($backendName)." ); + $ops[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand(0,50000) ); + $purgeOps[] = array( 'op' => 'delete', 'src' => $path ); + } + $purgeOps[] = array( 'op' => 'null' ); + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of source files succeeded ($backendName)." ); + + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file exists." ); + } + + $status = $this->backend->doQuickOperations( $purgeOps ); + $this->assertGoodStatus( $status, + "Quick deletion of source files succeeded ($backendName)." ); + + foreach ( $files as $file ) { + $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file purged." ); + } + } + /** * @dataProvider provider_testConcatenate */ @@ -667,7 +731,7 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { $backendName = $this->backendClass(); $expContent = ''; @@ -684,7 +748,7 @@ class FileBackendTest extends MediaWikiTestCase { } $status = $this->backend->doOperations( $ops ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of source files succeeded ($backendName)." ); $dest = $params['dst']; @@ -701,7 +765,7 @@ class FileBackendTest extends MediaWikiTestCase { // Combine the files into one $status = $this->backend->concatenate( $params ); if ( $okStatus ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of concat file at $dest succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of concat file at $dest succeeded ($backendName)." ); @@ -736,16 +800,16 @@ class FileBackendTest extends MediaWikiTestCase { $rand = mt_rand( 0, 2000000000 ) . time(); $dest = wfTempDir() . "/randomfile!$rand.txt"; $srcs = array( - $this->baseStorePath() . '/unittest-cont1/file1.txt', - $this->baseStorePath() . '/unittest-cont1/file2.txt', - $this->baseStorePath() . '/unittest-cont1/file3.txt', - $this->baseStorePath() . '/unittest-cont1/file4.txt', - $this->baseStorePath() . '/unittest-cont1/file5.txt', - $this->baseStorePath() . '/unittest-cont1/file6.txt', - $this->baseStorePath() . '/unittest-cont1/file7.txt', - $this->baseStorePath() . '/unittest-cont1/file8.txt', - $this->baseStorePath() . '/unittest-cont1/file9.txt', - $this->baseStorePath() . '/unittest-cont1/file10.txt' + $this->baseStorePath() . '/unittest-cont1/e/file1.txt', + $this->baseStorePath() . '/unittest-cont1/e/file2.txt', + $this->baseStorePath() . '/unittest-cont1/e/file3.txt', + $this->baseStorePath() . '/unittest-cont1/e/file4.txt', + $this->baseStorePath() . '/unittest-cont1/e/file5.txt', + $this->baseStorePath() . '/unittest-cont1/e/file6.txt', + $this->baseStorePath() . '/unittest-cont1/e/file7.txt', + $this->baseStorePath() . '/unittest-cont1/e/file8.txt', + $this->baseStorePath() . '/unittest-cont1/e/file9.txt', + $this->baseStorePath() . '/unittest-cont1/e/file10.txt' ); $content = array( 'egfage', @@ -800,8 +864,8 @@ class FileBackendTest extends MediaWikiTestCase { if ( $alreadyExists ) { $this->prepare( array( 'dir' => dirname( $path ) ) ); - $status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) ); - $this->assertEquals( array(), $status->errors, + $status = $this->create( array( 'dst' => $path, 'content' => $content ) ); + $this->assertGoodStatus( $status, "Creation of file at $path succeeded ($backendName)." ); $size = $this->backend->getFileSize( array( 'src' => $path ) ); @@ -810,20 +874,34 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( strlen( $content ), $size, "Correct file size of '$path'" ); - $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5, + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, "Correct file timestamp of '$path'" ); $size = $stat['size']; $time = $stat['mtime']; $this->assertEquals( strlen( $content ), $size, "Correct file size of '$path'" ); - $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5, + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, "Correct file timestamp of '$path'" ); + + $this->backend->clearCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + + $this->backend->preloadCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); } else { $size = $this->backend->getFileSize( array( 'src' => $path ) ); $time = $this->backend->getFileTimestamp( array( 'src' => $path ) ); $stat = $this->backend->getFileStat( array( 'src' => $path ) ); - + $this->assertFalse( $size, "Correct file size of '$path'" ); $this->assertFalse( $time, "Correct file timestamp of '$path'" ); $this->assertFalse( $stat, "Correct file stat of '$path'" ); @@ -834,9 +912,9 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents", true ); - $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "", true ); - $cases[] = array( "$base/unittest-cont1/b/some-diff_file.txt", null, false ); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false ); return $cases; } @@ -856,14 +934,14 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestGetFileContents( $source, $content ) { + private function doTestGetFileContents( $source, $content ) { $backendName = $this->backendClass(); $this->prepare( array( 'dir' => dirname( $source ) ) ); $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded with OK status ($backendName)." ); @@ -880,8 +958,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" ); return $cases; } @@ -901,14 +979,14 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); } - public function doTestGetLocalCopy( $source, $content ) { + private function doTestGetLocalCopy( $source, $content ) { $backendName = $this->backendClass(); $this->prepare( array( 'dir' => dirname( $source ) ) ); $status = $this->backend->doOperation( array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) ); @@ -923,8 +1001,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); return $cases; } @@ -949,9 +1027,8 @@ class FileBackendTest extends MediaWikiTestCase { $this->prepare( array( 'dir' => dirname( $source ) ) ); - $status = $this->backend->doOperation( - array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); - $this->assertEquals( array(), $status->errors, + $status = $this->create( array( 'content' => $content, 'dst' => $source ) ); + $this->assertGoodStatus( $status, "Creation of file at $source succeeded ($backendName)." ); $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) ); @@ -966,8 +1043,8 @@ class FileBackendTest extends MediaWikiTestCase { $cases = array(); $base = $this->baseStorePath(); - $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" ); - $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); return $cases; } @@ -988,19 +1065,19 @@ class FileBackendTest extends MediaWikiTestCase { function provider_testPrepareAndClean() { $base = $this->baseStorePath(); return array( - array( "$base/unittest-cont1/a/z/some_file1.txt", true ), + array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ), array( "$base/unittest-cont2/a/z/some_file2.txt", true ), # Specific to FS backend with no basePath field set #array( "$base/unittest-cont3/a/z/some_file3.txt", false ), ); } - function doTestPrepareAndClean( $path, $isOK ) { + private function doTestPrepareAndClean( $path, $isOK ) { $backendName = $this->backendClass(); $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); if ( $isOK ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Preparing dir $path succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Preparing dir $path succeeded ($backendName)." ); @@ -1011,7 +1088,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) ); if ( $isOK ) { - $this->assertEquals( array(), $status->errors, + $this->assertGoodStatus( $status, "Cleaning dir $path succeeded without warnings ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Cleaning dir $path succeeded ($backendName)." ); @@ -1021,6 +1098,58 @@ class FileBackendTest extends MediaWikiTestCase { } } + public function testRecursiveClean() { + $this->backend = $this->singleBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + } + + private function doTestRecursiveClean() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $dirs = array( + "$base/unittest-cont1/e/a", + "$base/unittest-cont1/e/a/b", + "$base/unittest-cont1/e/a/b/c", + "$base/unittest-cont1/e/a/b/c/d0", + "$base/unittest-cont1/e/a/b/c/d1", + "$base/unittest-cont1/e/a/b/c/d2", + "$base/unittest-cont1/e/a/b/c/d0/1", + "$base/unittest-cont1/e/a/b/c/d0/2", + "$base/unittest-cont1/e/a/b/c/d1/3", + "$base/unittest-cont1/e/a/b/c/d1/4", + "$base/unittest-cont1/e/a/b/c/d2/5", + "$base/unittest-cont1/e/a/b/c/d2/6" + ); + foreach ( $dirs as $dir ) { + $status = $this->prepare( array( 'dir' => $dir ) ); + $this->assertGoodStatus( $status, + "Preparing dir $dir succeeded without warnings ($backendName)." ); + } + + if ( $this->backend instanceof FSFileBackend ) { + foreach ( $dirs as $dir ) { + $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir exists ($backendName)." ); + } + } + + $status = $this->backend->clean( + array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) ); + $this->assertGoodStatus( $status, + "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." ); + + foreach ( $dirs as $dir ) { + $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir no longer exists ($backendName)." ); + } + } + // @TODO: testSecure public function testDoOperations() { @@ -1033,39 +1162,127 @@ class FileBackendTest extends MediaWikiTestCase { $this->tearDownFiles(); $this->doTestDoOperations(); $this->tearDownFiles(); + } + + private function doTestDoOperations() { + $base = $this->baseStorePath(); + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; + + $this->prepare( array( 'dir' => dirname( $fileA ) ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->prepare( array( 'dir' => dirname( $fileB ) ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->prepare( array( 'dir' => dirname( $fileC ) ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); + + $status = $this->backend->doOperations( array( + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), + // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>) + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:<A>, B:<B>, C:<A>, D:<empty> + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ), + // Now: A:<A>, B:<B>, C:<empty>, D:<A> + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ), + // Now: A:<A>, B:<empty>, C:<B>, D:<A> + array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:<A>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ), + // Now: A:<B>, B:<empty>, C:<empty>, D:<empty> + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ), + // Now: A:<B>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Now: A:<empty>, B:<empty>, C:<B>, D:<empty> + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'null' ), + // Does nothing + ) ); + $this->assertGoodStatus( $status, "Operation batch succeeded" ); + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); + $this->assertEquals( 13, count( $status->success ), + "Operation batch has correct success array" ); + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), + "File does not exist at $fileA" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), + "File does not exist at $fileB" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), + "File does not exist at $fileD" ); + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), + "File exists at $fileC" ); + $this->assertEquals( $fileBContents, + $this->backend->getFileContents( array( 'src' => $fileC ) ), + "Correct file contents of $fileC" ); + $this->assertEquals( strlen( $fileBContents ), + $this->backend->getFileSize( array( 'src' => $fileC ) ), + "Correct file size of $fileC" ); + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), + $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ), + "Correct file SHA-1 of $fileC" ); + } + + public function testDoOperationsPipeline() { $this->backend = $this->singleBackend; $this->tearDownFiles(); - $this->doTestDoOperationsFailing(); + $this->doTestDoOperationsPipeline(); $this->tearDownFiles(); $this->backend = $this->multiBackend; $this->tearDownFiles(); - $this->doTestDoOperationsFailing(); + $this->doTestDoOperationsPipeline(); $this->tearDownFiles(); - - // @TODO: test some cases where the ops should fail } - function doTestDoOperations() { + // concurrency orientated + private function doTestDoOperationsPipeline() { $base = $this->baseStorePath(); - $fileA = "$base/unittest-cont1/a/b/fileA.txt"; $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; - $fileB = "$base/unittest-cont1/a/b/fileB.txt"; $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; - $fileC = "$base/unittest-cont1/a/b/fileC.txt"; $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; - $fileD = "$base/unittest-cont1/a/b/fileD.txt"; + + $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameA, $fileAContents ); + $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameB, $fileBContents ); + $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameC, $fileCContents ); + + $this->filesToPrune[] = $tmpNameA; # avoid file leaking + $this->filesToPrune[] = $tmpNameB; # avoid file leaking + $this->filesToPrune[] = $tmpNameC; # avoid file leaking + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; $this->prepare( array( 'dir' => dirname( $fileA ) ) ); - $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); $this->prepare( array( 'dir' => dirname( $fileB ) ) ); - $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); $this->prepare( array( 'dir' => dirname( $fileC ) ) ); - $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); $status = $this->backend->doOperations( array( + array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ), array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>) array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), @@ -1094,9 +1311,9 @@ class FileBackendTest extends MediaWikiTestCase { // Does nothing ) ); - $this->assertEquals( array(), $status->errors, "Operation batch succeeded" ); + $this->assertGoodStatus( $status, "Operation batch succeeded" ); $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); - $this->assertEquals( 13, count( $status->success ), + $this->assertEquals( 16, count( $status->success ), "Operation batch has correct success array" ); $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), @@ -1119,7 +1336,19 @@ class FileBackendTest extends MediaWikiTestCase { "Correct file SHA-1 of $fileC" ); } - function doTestDoOperationsFailing() { + public function testDoOperationsFailing() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + } + + private function doTestDoOperationsFailing() { $base = $this->baseStorePath(); $fileA = "$base/unittest-cont2/a/b/fileA.txt"; @@ -1131,11 +1360,11 @@ class FileBackendTest extends MediaWikiTestCase { $fileD = "$base/unittest-cont2/a/b/fileD.txt"; $this->prepare( array( 'dir' => dirname( $fileA ) ) ); - $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); $this->prepare( array( 'dir' => dirname( $fileB ) ) ); - $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); $this->prepare( array( 'dir' => dirname( $fileC ) ) ); - $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); $status = $this->backend->doOperations( array( array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), @@ -1195,23 +1424,26 @@ class FileBackendTest extends MediaWikiTestCase { private function doTestGetFileList() { $backendName = $this->backendClass(); - $base = $this->baseStorePath(); + + // Should have no errors + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) ); + $files = array( - "$base/unittest-cont1/test1.txt", - "$base/unittest-cont1/test2.txt", - "$base/unittest-cont1/test3.txt", - "$base/unittest-cont1/subdir1/test1.txt", - "$base/unittest-cont1/subdir1/test2.txt", - "$base/unittest-cont1/subdir2/test3.txt", - "$base/unittest-cont1/subdir2/test4.txt", - "$base/unittest-cont1/subdir2/subdir/test1.txt", - "$base/unittest-cont1/subdir2/subdir/test2.txt", - "$base/unittest-cont1/subdir2/subdir/test3.txt", - "$base/unittest-cont1/subdir2/subdir/test4.txt", - "$base/unittest-cont1/subdir2/subdir/test5.txt", - "$base/unittest-cont1/subdir2/subdir/sub/test0.txt", - "$base/unittest-cont1/subdir2/subdir/sub/120-px-file.txt", + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir2/subdir/test2.txt", + "$base/unittest-cont1/e/subdir2/subdir/test3.txt", + "$base/unittest-cont1/e/subdir2/subdir/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test5.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt", ); // Add the files @@ -1220,28 +1452,28 @@ class FileBackendTest extends MediaWikiTestCase { $this->prepare( array( 'dir' => dirname( $file ) ) ); $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); } - $status = $this->backend->doOperations( $ops ); - $this->assertEquals( array(), $status->errors, + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, "Creation of files succeeded ($backendName)." ); $this->assertEquals( true, $status->isOK(), "Creation of files succeeded with OK status ($backendName)." ); // Expected listing $expected = array( - "test1.txt", - "test2.txt", - "test3.txt", - "subdir1/test1.txt", - "subdir1/test2.txt", - "subdir2/test3.txt", - "subdir2/test4.txt", - "subdir2/subdir/test1.txt", - "subdir2/subdir/test2.txt", - "subdir2/subdir/test3.txt", - "subdir2/subdir/test4.txt", - "subdir2/subdir/test5.txt", - "subdir2/subdir/sub/test0.txt", - "subdir2/subdir/sub/120-px-file.txt", + "e/test1.txt", + "e/test2.txt", + "e/test3.txt", + "e/subdir1/test1.txt", + "e/subdir1/test2.txt", + "e/subdir2/test3.txt", + "e/subdir2/test4.txt", + "e/subdir2/subdir/test1.txt", + "e/subdir2/subdir/test2.txt", + "e/subdir2/subdir/test3.txt", + "e/subdir2/subdir/test4.txt", + "e/subdir2/subdir/test5.txt", + "e/subdir2/subdir/sub/test0.txt", + "e/subdir2/subdir/sub/120-px-file.txt", ); sort( $expected ); @@ -1279,7 +1511,7 @@ class FileBackendTest extends MediaWikiTestCase { // Actual listing (no trailing slash) $list = array(); - $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); foreach ( $iter as $file ) { $list[] = $file; } @@ -1289,7 +1521,7 @@ class FileBackendTest extends MediaWikiTestCase { // Actual listing (with trailing slash) $list = array(); - $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir/" ) ); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) ); foreach ( $iter as $file ) { $list[] = $file; } @@ -1306,6 +1538,26 @@ class FileBackendTest extends MediaWikiTestCase { $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." ); + // Expected listing (top files only) + $expected = array( + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + "test5.txt" + ); + sort( $expected ); + + // Actual listing (top files only) + $list = array(); + $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." ); + foreach ( $files as $file ) { // clean up $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); } @@ -1314,12 +1566,268 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $iter as $iter ) {} // no errors } + public function testGetDirectoryList() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + } + + private function doTestGetDirectoryList() { + $backendName = $this->backendClass(); + + $base = $this->baseStorePath(); + $files = array( + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir3/subdir/test2.txt", + "$base/unittest-cont1/e/subdir4/subdir/test3.txt", + "$base/unittest-cont1/e/subdir4/subdir/test4.txt", + "$base/unittest-cont1/e/subdir4/subdir/test5.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt", + ); + + // Add the files + $ops = array(); + foreach ( $files as $file ) { + $this->prepare( array( 'dir' => dirname( $file ) ) ); + $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); + } + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of files succeeded with OK status ($backendName)." ); + + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( false, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ), + "Directory does not exists in ($backendName)." ); + + // Expected listing + $expected = array( + "e", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir1", + "subdir2", + "subdir3", + "subdir4", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (using iterator second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." ); + + // Expected listing (recursive) + $expected = array( + "e", + "e/subdir1", + "e/subdir2", + "e/subdir3", + "e/subdir4", + "e/subdir2/subdir", + "e/subdir3/subdir", + "e/subdir4/subdir", + "e/subdir4/subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Expected listing (recursive) + $expected = array( + "subdir", + "subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Actual listing (recursive, second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + foreach ( $files as $file ) { // clean up + $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); + } + + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) ); + foreach ( $iter as $iter ) {} // no errors + } + + public function testLockCalls() { + $this->backend = $this->singleBackend; + $this->doTestLockCalls(); + } + + private function doTestLockCalls() { + $backendName = $this->backendClass(); + + for ( $i=0; $i<50; $i++ ) { + $paths = array( + "test1.txt", + "test2.txt", + "test3.txt", + "subdir1", + "subdir1", // duplicate + "subdir1/test1.txt", + "subdir1/test2.txt", + "subdir2", + "subdir2", // duplicate + "subdir2/test3.txt", + "subdir2/test4.txt", + "subdir2/subdir", + "subdir2/subdir/test1.txt", + "subdir2/subdir/test2.txt", + "subdir2/subdir/test3.txt", + "subdir2/subdir/test4.txt", + "subdir2/subdir/test5.txt", + "subdir2/subdir/sub", + "subdir2/subdir/sub/test0.txt", + "subdir2/subdir/sub/120-px-file.txt", + ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( array(), $status->errors, + "Locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName)." ); + } + } + // test helper wrapper for backend prepare() function private function prepare( array $params ) { - $this->dirsToPrune[] = $params['dir']; return $this->backend->prepare( $params ); } + // test helper wrapper for backend prepare() function + private function create( array $params ) { + $params['op'] = 'create'; + return $this->backend->doQuickOperations( array( $params ) ); + } + function tearDownFiles() { foreach ( $this->filesToPrune as $file ) { @unlink( $file ); @@ -1328,10 +1836,7 @@ class FileBackendTest extends MediaWikiTestCase { foreach ( $containers as $container ) { $this->deleteFiles( $container ); } - foreach ( $this->dirsToPrune as $dir ) { - $this->recursiveClean( $dir ); - } - $this->filesToPrune = $this->dirsToPrune = array(); + $this->filesToPrune = array(); } private function deleteFiles( $container ) { @@ -1339,17 +1844,22 @@ class FileBackendTest extends MediaWikiTestCase { $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) ); if ( $iter ) { foreach ( $iter as $file ) { - $this->backend->delete( array( 'src' => "$base/$container/$file" ), array( 'force' => 1 ) ); + $this->backend->delete( array( 'src' => "$base/$container/$file" ), + array( 'force' => 1, 'nonLocking' => 1 ) ); } } + $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) ); } - private function recursiveClean( $dir ) { - do { - if ( !$this->backend->clean( array( 'dir' => $dir ) )->isOK() ) { - break; - } - } while ( $dir = FileBackend::parentStoragePath( $dir ) ); + function assertBackendPathsConsistent( array $paths ) { + if ( $this->backend instanceof FileBackendMultiWrite ) { + $status = $this->backend->consistencyCheck( $paths ); + $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) ); + } + } + + function assertGoodStatus( $status, $msg ) { + $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg ); } function tearDown() { diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php index 0f023138..8f92c123 100644 --- a/tests/phpunit/includes/filerepo/FileRepoTest.php +++ b/tests/phpunit/includes/filerepo/FileRepoTest.php @@ -34,8 +34,12 @@ class FileRepoTest extends MediaWikiTestCase { function testFileRepoConstructionWithRequiredOptions() { $f = new FileRepo( array( 'name' => 'FileRepoTestRepository', - 'backend' => 'local-backend', - )); + 'backend' => new FSFileBackend( array( + 'name' => 'local-testing', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array() + ) ) + ) ); $this->assertInstanceOf( 'FileRepo', $f ); } } diff --git a/tests/phpunit/includes/filerepo/StoreBatchTest.php b/tests/phpunit/includes/filerepo/StoreBatchTest.php index 6abceeb3..3ab56af8 100644 --- a/tests/phpunit/includes/filerepo/StoreBatchTest.php +++ b/tests/phpunit/includes/filerepo/StoreBatchTest.php @@ -1,6 +1,7 @@ <?php /** * @group FileRepo + * @group medium */ class StoreBatchTest extends MediaWikiTestCase { diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php new file mode 100644 index 00000000..54f66077 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSJanusTest.php @@ -0,0 +1,560 @@ +<?php +/** + * Based on the test suite of the original Python + * CSSJanus libary: + * http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus_test.py + * Ported to PHP for ResourceLoader and has been extended since. + */ +class CSSJanusTest extends MediaWikiTestCase { + /** + * @dataProvider provideTransformCases + */ + function testTransform( $cssA, $cssB = null ) { + + if ( $cssB ) { + $transformedA = CSSJanus::transform( $cssA ); + $this->assertEquals( $transformedA, $cssB, 'Test A-B transformation' ); + + $transformedB = CSSJanus::transform( $cssB ); + $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' ); + + // If no B version is provided, it means + // the output should equal the input. + } else { + $transformedA = CSSJanus::transform( $cssA ); + $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' ); + } + } + + /** + * @dataProvider provideTransformAdvancedCases + */ + function testTransformAdvanced( $code, $expectedOutput, $options = array() ) { + $swapLtrRtlInURL = isset( $options['swapLtrRtlInURL'] ) ? $options['swapLtrRtlInURL'] : false; + $swapLeftRightInURL = isset( $options['swapLeftRightInURL'] ) ? $options['swapLeftRightInURL'] : false; + + $flipped = CSSJanus::transform( $code, $swapLtrRtlInURL, $swapLeftRightInURL ); + + $this->assertEquals( $expectedOutput, $flipped, + 'Test flipping, options: url-ltr-rtl=' . ($swapLtrRtlInURL ? 'true' : 'false') + . ' url-left-right=' . ($swapLeftRightInURL ? 'true' : 'false') + ); + } + /** + * @dataProvider provideTransformBrokenCases + * @group Broken + */ + function testTransformBroken( $code, $expectedOutput ) { + $flipped = CSSJanus::transform( $code ); + + $this->assertEquals( $expectedOutput, $flipped, 'Test flipping' ); + } + + /** + * These transform cases are tested *in both directions* + * No need to declare a principle twice in both directions here. + */ + function provideTransformCases() { + return array( + // Property keys + array( + '.foo { left: 0; }', + '.foo { right: 0; }' + ), + // Guard against partial keys + // (CSS currently doesn't have flippable properties + // that contain the direction as part of the key without + // dash separation) + array( + '.foo { alright: 0; }' + ), + array( + '.foo { balleft: 0; }' + ), + + // Dashed property keys + array( + '.foo { padding-left: 0; }', + '.foo { padding-right: 0; }' + ), + array( + '.foo { margin-left: 0; }', + '.foo { margin-right: 0; }' + ), + array( + '.foo { border-left: 0; }', + '.foo { border-right: 0; }' + ), + + // Double-dashed property keys + array( + '.foo { border-left-color: red; }', + '.foo { border-right-color: red; }' + ), + array( + // Includes unknown properties? + '.foo { x-left-y: 0; }', + '.foo { x-right-y: 0; }' + ), + + // Multi-value properties + array( + '.foo { padding: 0; }' + ), + array( + '.foo { padding: 0 1px; }' + ), + array( + '.foo { padding: 0 1px 2px; }' + ), + array( + '.foo { padding: 0 1px 2px 3px; }', + '.foo { padding: 0 3px 2px 1px; }' + ), + + // Shorthand / Four notation + array( + '.foo { padding: .25em 15px 0pt 0ex; }', + '.foo { padding: .25em 0ex 0pt 15px; }' + ), + array( + '.foo { margin: 1px -4px 3px 2px; }', + '.foo { margin: 1px 2px 3px -4px; }' + ), + array( + '.foo { padding: 0 15px .25em 0; }', + '.foo { padding: 0 0 .25em 15px; }' + ), + array( + '.foo { padding: 1px 4.1grad 3px 2%; }', + '.foo { padding: 1px 2% 3px 4.1grad; }' + ), + array( + '.foo { padding: 1px 2px 3px auto; }', + '.foo { padding: 1px auto 3px 2px; }' + ), + array( + '.foo { padding: 1px inherit 3px auto; }', + '.foo { padding: 1px auto 3px inherit; }' + ), + array( + '.foo { border-radius: .25em 15px 0pt 0ex; }', + '.foo { border-radius: .25em 0ex 0pt 15px; }' + ), + array( + '.foo { x-unknown: a b c d; }' + ), + array( + '.foo barpx 0 2% { opacity: 0; }' + ), + array( + '#settings td p strong' + ), + array( + # Not sure how 4+ values should behave, + # testing to make sure changes are detected + '.foo { x-unknown: 1 2 3 4 5; }', + '.foo { x-unknown: 1 4 3 2 5; }', + ), + array( + '.foo { x-unknown: 1 2 3 4 5 6; }', + '.foo { x-unknown: 1 4 3 2 5 6; }', + ), + + // Shorthand / Three notation + array( + '.foo { margin: 1em 0 .25em; }' + ), + array( + '.foo { margin:-1.5em 0 -.75em; }' + ), + + // Shorthand / Two notation + array( + '.foo { padding: 1px 2px; }' + ), + + // Shorthand / One notation + array( + '.foo { padding: 1px; }' + ), + + // Direction + // Note: This differs from the Python implementation, + // see also CSSJanus::fixDirection for more info. + array( + '.foo { direction: ltr; }', + '.foo { direction: rtl; }' + ), + array( + '.foo { direction: rtl; }', + '.foo { direction: ltr; }' + ), + array( + 'input { direction: ltr; }', + 'input { direction: rtl; }' + ), + array( + 'input { direction: rtl; }', + 'input { direction: ltr; }' + ), + array( + 'body { direction: ltr; }', + 'body { direction: rtl; }' + ), + array( + '.foo, body, input { direction: ltr; }', + '.foo, body, input { direction: rtl; }' + ), + array( + 'body { padding: 10px; direction: ltr; }', + 'body { padding: 10px; direction: rtl; }' + ), + array( + 'body { direction: ltr } .myClass { direction: ltr }', + 'body { direction: rtl } .myClass { direction: rtl }' + ), + + // Left/right values + array( + '.foo { float: left; }', + '.foo { float: right; }' + ), + array( + '.foo { text-align: left; }', + '.foo { text-align: right; }' + ), + array( + '.foo { -x-unknown: left; }', + '.foo { -x-unknown: right; }' + ), + // Guard against selectors that look flippable + array( + '.column-left { width: 0; }' + ), + array( + 'a.left { width: 0; }' + ), + array( + 'a.leftification { width: 0; }' + ), + array( + 'a.ltr { width: 0; }' + ), + array( + # <div class="a-ltr png"> + '.a-ltr.png { width: 0; }' + ), + array( + # <foo-ltr attr="x"> + 'foo-ltr[attr="x"] { width: 0; }' + ), + array( + 'div.left > span.right+span.left { width: 0; }' + ), + array( + '.thisclass .left .myclass { width: 0; }' + ), + array( + '.thisclass .left .myclass #myid { width: 0; }' + ), + + // Cursor values (east/west) + array( + '.foo { cursor: e-resize; }', + '.foo { cursor: w-resize; }' + ), + array( + '.foo { cursor: se-resize; }', + '.foo { cursor: sw-resize; }' + ), + array( + '.foo { cursor: ne-resize; }', + '.foo { cursor: nw-resize; }' + ), + + // Background + array( + '.foo { background-position: top left; }', + '.foo { background-position: top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left; }', + '.foo { background: url(/foo/bar.png) top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left no-repeat; }', + '.foo { background: url(/foo/bar.png) top right no-repeat; }' + ), + array( + '.foo { background: url(/foo/bar.png) no-repeat top left; }', + '.foo { background: url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background: #fff url(/foo/bar.png) no-repeat top left; }', + '.foo { background: #fff url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background-position: 100% 40%; }', + '.foo { background-position: 0% 40%; }' + ), + array( + '.foo { background-position: 23% 0; }', + '.foo { background-position: 77% 0; }' + ), + array( + '.foo { background-position: 23% auto; }', + '.foo { background-position: 77% auto; }' + ), + array( + '.foo { background-position-x: 23%; }', + '.foo { background-position-x: 77%; }' + ), + array( + '.foo { background-position-y: 23%; }', + '.foo { background-position-y: 23%; }' + ), + array( + '.foo { background:url(../foo.png) no-repeat 75% 50%; }', + '.foo { background:url(../foo.png) no-repeat 25% 50%; }' + ), + array( + '.foo { background: 10% 20% } .bar { background: 40% 30% }', + '.foo { background: 90% 20% } .bar { background: 60% 30% }' + ), + + // Multiple rules + array( + 'body { direction: rtl; float: right; } .foo { direction: ltr; float: right; }', + 'body { direction: ltr; float: left; } .foo { direction: rtl; float: left; }', + ), + + // Duplicate properties + array( + '.foo { float: left; float: right; float: left; }', + '.foo { float: right; float: left; float: right; }', + ), + + // Preserve comments + array( + '/* left /* right */left: 10px', + '/* left /* right */right: 10px' + ), + array( + '/*left*//*left*/left: 10px', + '/*left*//*left*/right: 10px' + ), + array( + '/* Going right is cool */ .foo { width: 0 }', + ), + array( + "/* padding-right 1 2 3 4 */\n#test { width: 0}\n/*right*/" + ), + array( + "/** Two line comment\n * left\n \*/\n#test {width: 0}" + ), + + // @noflip annotation + array( + // before selector (single) + '/* @noflip */ div { float: left; }' + ), + array( + // before selector (multiple) + '/* @noflip */ div, .notme { float: left; }' + ), + array( + // inside selector + 'div, /* @noflip */ .foo { float: left; }' + ), + array( + // after selector + 'div, .notme /* @noflip */ { float: left; }' + ), + array( + // before multiple rules + '/* @noflip */ div { float: left; } .foo { float: left; }', + '/* @noflip */ div { float: left; } .foo { float: right; }' + ), + array( + // after multiple rules + '.foo { float: left; } /* @noflip */ div { float: left; }', + '.foo { float: right; } /* @noflip */ div { float: left; }' + ), + array( + // before multiple properties + 'div { /* @noflip */ float: left; text-align: left; }', + 'div { /* @noflip */ float: left; text-align: right; }' + ), + array( + // after multiple properties + 'div { float: left; /* @noflip */ text-align: left; }', + 'div { float: right; /* @noflip */ text-align: left; }' + ), + + // Guard against css3 stuff + array( + 'background-image: -moz-linear-gradient(#326cc1, #234e8c);' + ), + array( + 'background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff));' + ), + + // CSS syntax / white-space variations + // spaces, no spaces, tabs, new lines, omitting semi-colons + array( + ".foo { left: 0; }", + ".foo { right: 0; }" + ), + array( + ".foo{ left: 0; }", + ".foo{ right: 0; }" + ), + array( + ".foo{ left: 0 }", + ".foo{ right: 0 }" + ), + array( + ".foo{left:0 }", + ".foo{right:0 }" + ), + array( + ".foo{left:0}", + ".foo{right:0}" + ), + array( + ".foo { left : 0 ; }", + ".foo { right : 0 ; }" + ), + array( + ".foo\n { left : 0 ; }", + ".foo\n { right : 0 ; }" + ), + array( + ".foo\n { \nleft : 0 ; }", + ".foo\n { \nright : 0 ; }" + ), + array( + ".foo\n { \n left : 0 ; }", + ".foo\n { \n right : 0 ; }" + ), + array( + ".foo\n { \n left\n : 0; }", + ".foo\n { \n right\n : 0; }" + ), + array( + ".foo \n { \n left\n : 0; }", + ".foo \n { \n right\n : 0; }" + ), + array( + ".foo\n{\nleft\n:\n0;}", + ".foo\n{\nright\n:\n0;}" + ), + array( + ".foo\n.bar {\n\tleft: 0;\n}", + ".foo\n.bar {\n\tright: 0;\n}" + ), + array( + ".foo\t{\tleft\t:\t0;}", + ".foo\t{\tright\t:\t0;}" + ), + ); + } + + /** + * These cases are tested in one way only (format: actual, expected, msg). + * If both ways can be tested, either put both versions in here or move + * it to provideTransformCases(). + */ + function provideTransformAdvancedCases() { + $bgPairs = array( + # [ - _ . ] <-> [ left right ltr rtl ] + 'foo.jpg' => 'foo.jpg', + 'left.jpg' => 'right.jpg', + 'ltr.jpg' => 'rtl.jpg', + + 'foo-left.png' => 'foo-right.png', + 'foo_left.png' => 'foo_right.png', + 'foo.left.png' => 'foo.right.png', + + 'foo-ltr.png' => 'foo-rtl.png', + 'foo_ltr.png' => 'foo_rtl.png', + 'foo.ltr.png' => 'foo.rtl.png', + + 'left-foo.png' => 'right-foo.png', + 'left_foo.png' => 'right_foo.png', + 'left.foo.png' => 'right.foo.png', + + 'ltr-foo.png' => 'rtl-foo.png', + 'ltr_foo.png' => 'rtl_foo.png', + 'ltr.foo.png' => 'rtl.foo.png', + + 'foo-ltr-left.gif' => 'foo-rtl-right.gif', + 'foo_ltr_left.gif' => 'foo_rtl_right.gif', + 'foo.ltr.left.gif' => 'foo.rtl.right.gif', + 'foo-ltr_left.gif' => 'foo-rtl_right.gif', + 'foo_ltr.left.gif' => 'foo_rtl.right.gif', + ); + $provider = array(); + foreach ( $bgPairs as $left => $right ) { + # By default '-rtl' and '-left' etc. are not touched, + # Only when the appropiate parameter is set. + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$left); }" + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$right); }" + ); + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$right); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$left); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + } + + return $provider; + } + + /** + * Cases that are currently failing, but + * should be looked at in the future as enhancements and/or bug fix + */ + function provideTransformBrokenCases() { + return array( + // Guard against partial keys + array( + '.foo { leftxx: 0; }', + '.foo { leftxx: 0; }' + ), + array( + '.foo { rightxx: 0; }', + '.foo { rightxx: 0; }' + ), + + // Guard against selectors that look flippable + array( + # <foo-left-x attr="x"> + 'foo-left-x[attr="x"] { width: 0; }', + 'foo-left-x[attr="x"] { width: 0; }' + ), + array( + # <div class="foo" data-left="x"> + '.foo[data-left="x"] { width: 0; }', + '.foo[data-left="x"] { width: 0; }' + ), + ); + } +} diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php new file mode 100644 index 00000000..a3827756 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -0,0 +1,142 @@ +<?php +/** + * This file test the CSSMin library shipped with Mediawiki. + * + * @author Timo Tijhof + */ + +class CSSMinTest extends MediaWikiTestCase { + protected $oldServer = null, $oldCanServer = null; + + function setUp() { + parent::setUp(); + + // Fake $wgServer and $wgCanonicalServer + global $wgServer, $wgCanonicalServer; + $this->oldServer = $wgServer; + $this->oldCanServer = $wgCanonicalServer; + $wgServer = $wgCanonicalServer = 'http://wiki.example.org'; + } + + function tearDown() { + // Restore $wgServer and $wgCanonicalServer + global $wgServer, $wgCanonicalServer; + $wgServer = $this->oldServer; + $wgCanonicalServer = $this->oldCanServer; + + parent::tearDown(); + } + + /** + * @dataProvider provideMinifyCases + */ + function testMinify( $code, $expectedOutput ) { + $minified = CSSMin::minify( $code ); + + $this->assertEquals( $expectedOutput, $minified, 'Minified output should be in the form expected.' ); + } + + function provideMinifyCases() { + return array( + // Whitespace + array( "\r\t\f \v\n\r", "" ), + array( "foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + + // Loose comments + array( "/* foo */", "" ), + array( "/*******\n foo\n *******/", "" ), + array( "/*!\n foo\n */", "" ), + + // Inline comments in various different places + array( "/* comment */foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo/* comment */, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo,/* comment */ bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar/* comment */ {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\t/* comment */prop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: /* comment */value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: value /* comment */;\n}", "foo,bar{prop:value }" ), + array( "foo, bar {\n\tprop: value; /* comment */\n}", "foo,bar{prop:value; }" ), + + // Keep track of things that aren't as minified as much as they + // could be (bug 35493) + array( 'foo { prop: value ;}', 'foo{prop:value }' ), + array( 'foo { prop : value; }', 'foo{prop :value}' ), + array( 'foo { prop: value ; }', 'foo{prop:value }' ), + array( 'foo { font-family: "foo" , "bar"; }', 'foo{font-family:"foo" ,"bar"}' ), + array( "foo { src:\n\turl('foo') ,\n\turl('bar') ; }", "foo{src:url('foo') ,url('bar') }" ), + + // Interesting cases with string values + // - Double quotes, single quotes + array( 'foo { content: ""; }', 'foo{content:""}' ), + array( "foo { content: ''; }", "foo{content:''}" ), + array( 'foo { content: "\'"; }', 'foo{content:"\'"}' ), + array( "foo { content: '\"'; }", "foo{content:'\"'}" ), + // - Whitespace in string values + array( 'foo { content: " "; }', 'foo{content:" "}' ), + ); + } + + /** + * @dataProvider provideRemapCases + */ + function testRemap( $message, $params, $expectedOutput ) { + $remapped = call_user_func_array( 'CSSMin::remap', $params ); + + $messageAdd = " Case: $message"; + $this->assertEquals( $expectedOutput, $remapped, 'CSSMin::remap should return the expected url form.' . $messageAdd ); + } + + function provideRemapCases() { + // Parameter signature: + // CSSMin::remap( $code, $local, $remote, $embedData = true ) + return array( + array( + 'Simple case', + array( 'foo { prop: url(bar.png); }', false, 'http://example.org', false ), + 'foo { prop: url(http://example.org/bar.png); }', + ), + array( + 'Without trailing slash', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'With trailing slash on remote (bug 27052)', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'Guard against stripping double slashes from query', + array( 'foo { prop: url(bar.png?corge=//grault); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/bar.png?corge=//grault); }', + ), + array( + 'Expand absolute paths', + array( 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://wiki.example.org/w/skin/images/bar.png); }', + ), + ); + } + + /** + * Seperated because they are currently broken (bug 35492) + * + * @group Broken + * @dataProvider provideStringCases + */ + function testMinifyWithCSSStringValues( $code, $expectedOutput ) { + $this->testMinifyOutput( $code, $expectedOutput ); + } + + function provideStringCases() { + return array( + // String values should be respected + // - More than one space in a string value + array( 'foo { content: " "; }', 'foo{content:" "}' ), + // - Using a tab in a string value (turns into a space) + array( "foo { content: '\t'; }", "foo{content:'\t'}" ), + // - Using css-like syntax in string values + array( 'foo::after { content: "{;}"; position: absolute; }', 'foo::after{content:"{;}";position:absolute}' ), + ); + } +} diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php new file mode 100644 index 00000000..70fce111 --- /dev/null +++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php @@ -0,0 +1,245 @@ +<?php + +/** + * Tests for the GenericArrayObject and deriving classes. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.20 + * + * @ingroup Test + * @group GenericArrayObject + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class GenericArrayObjectTest extends MediaWikiTestCase { + + /** + * Returns objects that can serve as elements in the concrete GenericArrayObject deriving class being tested. + * + * @since 1.20 + * + * @return array + */ + public abstract function elementInstancesProvider(); + + /** + * Returns the name of the concrete class being tested. + * + * @since 1.20 + * + * @return string + */ + public abstract function getInstanceClass(); + + /** + * Provides instances of the concrete class being tested. + * + * @since 1.20 + * + * @return array + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->elementInstancesProvider() as $elementInstances ) { + $instances[] = $this->getNew( $elementInstances ); + } + + return $this->arrayWrap( $instances ); + } + + /** + * @since 1.20 + * + * @param array $elements + * + * @return GenericArrayObject + */ + protected function getNew( array $elements = array() ) { + $class = $this->getInstanceClass(); + return new $class( $elements ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testConstructor( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( count( $elements ), $arrayObject->count() ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testIsEmpty( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( $elements === array(), $arrayObject->isEmpty() ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.20 + * + * @param GenericArrayObject $list + */ + public function testUnset( GenericArrayObject $list ) { + if ( !$list->isEmpty() ) { + $offset = $list->getIterator()->key(); + $count = $list->count(); + $list->offsetUnset( $offset ); + $this->assertEquals( $count - 1, $list->count() ); + } + + if ( !$list->isEmpty() ) { + $offset = $list->getIterator()->key(); + $count = $list->count(); + unset( $list[$offset] ); + $this->assertEquals( $count - 1, $list->count() ); + } + + $exception = null; + try { $list->offsetUnset( 'sdfsedtgsrdysftu' ); } catch ( \Exception $exception ){} + $this->assertInstanceOf( '\Exception', $exception ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testAppend( array $elements ) { + $list = $this->getNew(); + + $listSize = count( $elements ); + + foreach ( $elements as $element ) { + $list->append( $element ); + } + + $this->assertEquals( $listSize, $list->count() ); + + $list = $this->getNew(); + + foreach ( $elements as $element ) { + $list[] = $element; + } + + $this->assertEquals( $listSize, $list->count() ); + + $this->checkTypeChecks( function( GenericArrayObject $list, $element ) { + $list->append( $element ); + } ); + } + + /** + * @since 1.20 + * + * @param callback $function + */ + protected function checkTypeChecks( $function ) { + $excption = null; + $list = $this->getNew(); + + $elementClass = $list->getObjectType(); + + foreach ( array( 42, 'foo', array(), new \stdClass(), 4.2 ) as $element ) { + $validValid = $element instanceof $elementClass; + + try{ + call_user_func( $function, $list, $element ); + $valid = true; + } + catch ( InvalidArgumentException $exception ) { + $valid = false; + } + + $this->assertEquals( + $validValid, + $valid, + 'Object of invalid type got successfully added to a GenericArrayObject' + ); + } + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testOffsetSet( array $elements ) { + if ( $elements === array() ) { + $this->assertTrue( true ); + return; + } + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 42, $element ); + $this->assertEquals( $element, $list->offsetGet( 42 ) ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list['oHai'] = $element; + $this->assertEquals( $element, $list['oHai'] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 9001, $element ); + $this->assertEquals( $element, $list[9001] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[0] ); + + $list = $this->getNew(); + $offset = 0; + + foreach ( $elements as $element ) { + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[$offset++] ); + } + + $this->assertEquals( count( $elements ), $list->count() ); + + $this->checkTypeChecks( function( GenericArrayObject $list, $element ) { + $list->offsetSet( mt_rand(), $element ); + } ); + } + +} diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php index d2bfeedf..f121b018 100644 --- a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php +++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -4,9 +4,18 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { function provideCases() { return array( - // Basic tokens + + // Basic whitespace and comments that should be stripped entirely array( "\r\t\f \v\n\r", "" ), array( "/* Foo *\n*bar\n*/", "" ), + + /** + * Slashes used inside block comments (bug 26931). + * At some point there was a bug that caused this comment to be ended at '* /', + * causing /M... to be left as the beginning of a regex. + */ + array( "/**\n * Foo\n * {\n * 'bar' : {\n * //Multiple rules with configurable operators\n * 'baz' : false\n * }\n */", ""), + /** * ' Foo \' bar \ * baz \' quox ' . @@ -15,11 +24,13 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "\" Foo \\\" bar \\\n baz \\\" quox \" .length", "\" Foo \\\" bar \\\n baz \\\" quox \".length" ), array( "// Foo b/ar baz", "" ), array( "/ Foo \\/ bar [ / \\] / ] baz / .length", "/ Foo \\/ bar [ / \\] / ] baz /.length" ), + // HTML comments array( "<!-- Foo bar", "" ), array( "<!-- Foo --> bar", "" ), array( "--> Foo", "" ), array( "x --> y", "x-->y" ), + // Semicolon insertion array( "(function(){return\nx;})", "(function(){return\nx;})" ), array( "throw\nx;", "throw\nx;" ), @@ -35,12 +46,19 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "5.\nx;", "5.\nx;" ), array( "0xFF.\nx;", "0xFF.x;" ), array( "5.3.\nx;", "5.3.x;" ), + + // Semicolon insertion between an expression having an inline + // comment after it, and a statement on the next line (bug 27046). + array( "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}", "var a=this\nfor(b=0;c<d;b++){}" ), + // Token separation array( "x in y", "x in y" ), array( "/x/g in y", "/x/g in y" ), array( "x in 30", "x in 30" ), array( "x + ++ y", "x+ ++y" ), + array( "x ++ + y", "x++ +y" ), array( "x / /y/.exec(z)", "x/ /y/.exec(z)" ), + // State machine array( "/ x/g", "/ x/g" ), array( "(function(){return/ x/g})", "(function(){return/ x/g})" ), @@ -63,15 +81,18 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { array( "function x(){}/ x/g", "function x(){}/ x/g" ), array( "+function x(){}/ x/g", "+function x(){}/x/g" ), - // Tests for things that broke in the past // Multiline quoted string array( "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ), + // Multiline quoted string followed by string with spaces array( "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n", "var foo=\"\\\nblah\\\n\";var baz=\" foo \";" ), + // URL in quoted string ( // is not a comment) array( "aNode.setAttribute('href','http://foo.bar.org/baz');", "aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // URL in quoted string after multiline quoted string array( "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');", "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // Division vs. regex nastiness array( "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );", "alert((10+10)/'/'.charCodeAt(0)+'//');" ), array( "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ), @@ -81,11 +102,12 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { // Unicode letter characters should pass through ok in identifiers (bug 31187) array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}'), - // And also per spec unicode char escape values should work in identifiers, + + // Per spec unicode char escape values should work in identifiers, // as long as it's a valid char. In future it might get normalized. array( "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}'), - /* Some structures that might look invalid at first sight */ + // Some structures that might look invalid at first sight array( "var a = 5.;", "var a=5.;" ), array( "5.0.toString();", "5.0.toString();" ), array( "5..toString();", "5..toString();" ), @@ -110,24 +132,6 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." ); } - /** - * @dataProvider provideBug32548 - */ - function testBug32548Exponent($num) { - // Long line breaking was being incorrectly done between the base and - // exponent part of a number, causing a syntax error. The line should - // instead break at the start of the number. - $prefix = 'var longVarName' . str_repeat('_', 973) . '='; - $suffix = ',shortVarName=0;'; - - $input = $prefix . $num . $suffix; - $expected = $prefix . "\n" . $num . $suffix; - - $minified = JavaScriptMinifier::minify( $input ); - - $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent"); - } - function provideBug32548() { return array( array( @@ -145,4 +149,22 @@ class JavaScriptMinifierTest extends MediaWikiTestCase { ), ); } + + /** + * @dataProvider provideBug32548 + */ + function testBug32548Exponent( $num ) { + // Long line breaking was being incorrectly done between the base and + // exponent part of a number, causing a syntax error. The line should + // instead break at the start of the number. + $prefix = 'var longVarName' . str_repeat( '_', 973 ) . '='; + $suffix = ',shortVarName=0;'; + + $input = $prefix . $num . $suffix; + $expected = $prefix . "\n" . $num . $suffix; + + $minified = JavaScriptMinifier::minify( $input ); + + $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent"); + } } diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php index f4f52dd8..88f87ef9 100644 --- a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php +++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php @@ -2,7 +2,7 @@ class BitmapMetadataHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** @@ -73,7 +73,8 @@ class BitmapMetadataHandlerTest extends MediaWikiTestCase { $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] ); $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] ); } - /* File has an invalid time (+ one valid but really weird time) + /** + * File has an invalid time (+ one valid but really weird time) * that shouldn't be included */ public function testIPTCDatesInvalid() { diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php index 25149a05..6af52dd1 100644 --- a/tests/phpunit/includes/media/ExifRotationTest.php +++ b/tests/phpunit/includes/media/ExifRotationTest.php @@ -5,16 +5,12 @@ */ class ExifRotationTest extends MediaWikiTestCase { - /** track directories creations. Content will be deleted. */ - private $createdDirs = array(); - function setUp() { parent::setUp(); $this->handler = new BitmapHandler(); - $filePath = dirname( __FILE__ ) . '/../../data/media'; + $filePath = __DIR__ . '/../../data/media'; - $tmpDir = wfTempDir() . '/exif-test-' . time() . '-' . mt_rand(); - $this->createdDirs[] = $tmpDir; + $tmpDir = $this->getNewTempDirectory(); $this->repo = new FSRepo( array( 'name' => 'temp', @@ -42,17 +38,7 @@ class ExifRotationTest extends MediaWikiTestCase { $wgShowEXIF = $this->show; $wgEnableAutoRotation = $this->oldAuto; - $this->tearDownFiles(); - } - - private function tearDownFiles() { - foreach( $this->createdDirs as $dir ) { - wfRecursiveRemoveDir( $dir ); - } - } - - function __destruct() { - $this->tearDownFiles(); + parent::tearDown(); } /** diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php index b39c15e4..045777d7 100644 --- a/tests/phpunit/includes/media/ExifTest.php +++ b/tests/phpunit/includes/media/ExifTest.php @@ -2,22 +2,22 @@ class ExifTest extends MediaWikiTestCase { public function setUp() { - $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + $this->mediaPath = __DIR__ . '/../../data/media/'; if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } - global $wgShowEXIF; - $this->showExif = $wgShowEXIF; - $wgShowEXIF = true; + global $wgShowEXIF; + $this->showExif = $wgShowEXIF; + $wgShowEXIF = true; } - public function tearDown() { - global $wgShowEXIF; - $wgShowEXIF = $this->showExif; - } - public function testGPSExtraction() { + public function tearDown() { + global $wgShowEXIF; + $wgShowEXIF = $this->showExif; + } + public function testGPSExtraction() { $filename = $this->mediaPath . 'exif-gps.jpg'; $seg = JpegMetadataExtractor::segmentSplitter( $filename ); $exif = new Exif( $filename, $seg['byteOrder'] ); @@ -25,14 +25,14 @@ class ExifTest extends MediaWikiTestCase { $expected = array( 'GPSLatitude' => 88.5180555556, 'GPSLongitude' => -21.12357, - 'GPSAltitude' => -200, + 'GPSAltitude' => -3.141592653, 'GPSDOP' => '5/1', 'GPSVersionID' => '2.2.0.0', ); $this->assertEquals( $expected, $data, '', 0.0000000001 ); } - public function testUnicodeUserComment() { + public function testUnicodeUserComment() { $filename = $this->mediaPath . 'exif-user-comment.jpg'; $seg = JpegMetadataExtractor::segmentSplitter( $filename ); $exif = new Exif( $filename, $seg['byteOrder'] ); diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php index 8a632f52..6ade6702 100644 --- a/tests/phpunit/includes/media/FormatMetadataTest.php +++ b/tests/phpunit/includes/media/FormatMetadataTest.php @@ -4,7 +4,7 @@ class FormatMetadataTest extends MediaWikiTestCase { if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } - $filePath = dirname( __FILE__ ) . '/../../data/media'; + $filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php index 47fc368b..650fdd5c 100644 --- a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -2,7 +2,7 @@ class GIFMetadataExtractorTest extends MediaWikiTestCase { public function setUp() { - $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + $this->mediaPath = __DIR__ . '/../../data/media/'; } /** * Put in a file, and see if the metadata coming out is as expected. diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php index 36658358..5dcbeee0 100644 --- a/tests/phpunit/includes/media/GIFTest.php +++ b/tests/phpunit/includes/media/GIFTest.php @@ -2,7 +2,7 @@ class GIFHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media'; + $this->filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php index f48382a4..41d81190 100644 --- a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -9,7 +9,7 @@ class JpegMetadataExtractorTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php index ddabf5b8..ea007f90 100644 --- a/tests/phpunit/includes/media/JpegTest.php +++ b/tests/phpunit/includes/media/JpegTest.php @@ -2,7 +2,7 @@ class JpegTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; if ( !wfDl( 'exif' ) ) { $this->markTestSkipped( "This test needs the exif extension." ); } diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php index 9f702c50..1b1b2ec3 100644 --- a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -2,7 +2,7 @@ class PNGMetadataExtractorTest extends MediaWikiTestCase { function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; } /** * Tests zTXt tag (compressed textual metadata) diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php index b6f911fd..fe73c9c7 100644 --- a/tests/phpunit/includes/media/PNGTest.php +++ b/tests/phpunit/includes/media/PNGTest.php @@ -2,7 +2,7 @@ class PNGHandlerTest extends MediaWikiTestCase { public function setUp() { - $this->filePath = dirname( __FILE__ ) . '/../../data/media'; + $this->filePath = __DIR__ . '/../../data/media'; $this->backend = new FSFileBackend( array( 'name' => 'localtesting', 'lockManager' => 'nullLockManager', diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php index 526beae8..2116554e 100644 --- a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -39,27 +39,33 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { } function providerSvgFiles() { - $base = dirname( __FILE__ ) . '/../../data/media'; + $base = __DIR__ . '/../../data/media'; return array( array( "$base/Wikimedia-logo.svg", array( 'width' => 1024, - 'height' => 1024 + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', ) ), array( "$base/QA_icon.svg", array( 'width' => 60, - 'height' => 60 + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', ) ), array( "$base/Gtk-media-play-ltr.svg", array( 'width' => 60, - 'height' => 60 + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', ) ), array( @@ -67,14 +73,16 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { // This file triggered bug 31719, needs entity expansion in the xmlns checks array( 'width' => 385, - 'height' => 385 + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', ) ) ); } function providerSvgFilesWithXMLMetadata() { - $base = dirname( __FILE__ ) . '/../../data/media'; + $base = __DIR__ . '/../../data/media'; $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about=""> @@ -89,7 +97,9 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { array( 'height' => 593, 'metadata' => $metadata, - 'width' => 959 + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', ) ), ); diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php index d4cf503b..4c79f66c 100644 --- a/tests/phpunit/includes/media/TiffTest.php +++ b/tests/phpunit/includes/media/TiffTest.php @@ -5,7 +5,7 @@ class TiffTest extends MediaWikiTestCase { global $wgShowEXIF; $this->showExif = $wgShowEXIF; $wgShowEXIF = true; - $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->filePath = __DIR__ . '/../../data/media/'; $this->handler = new TiffHandler; } diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php index c952b23c..8198d3b0 100644 --- a/tests/phpunit/includes/media/XMPTest.php +++ b/tests/phpunit/includes/media/XMPTest.php @@ -22,11 +22,11 @@ class XMPTest extends MediaWikiTestCase { } $reader = new XMPReader; $reader->parse( $xmp ); - $this->assertEquals( $expected, $reader->getResults(), $info ); + $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 ); } public function dataXMPParse() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/' ; + $xmpPath = __DIR__ . '/../../data/xmp/' ; $data = array(); // $xmpFiles format: array of arrays with first arg file base name, @@ -52,6 +52,7 @@ class XMPTest extends MediaWikiTestCase { array( 'utf32BE', 'UTF-32BE encoding' ), array( 'utf32LE', 'UTF-32LE encoding' ), array( 'xmpExt', 'Extended XMP missing second part' ), + array( 'gps', 'Handling of exif GPS parameters in XMP' ), ); foreach( $xmpFiles as $file ) { $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' ); @@ -72,7 +73,7 @@ class XMPTest extends MediaWikiTestCase { * world example file to double check the support for this is right. */ function testExtendedXMP() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); @@ -102,7 +103,7 @@ class XMPTest extends MediaWikiTestCase { * and thus should only return the StandardXMP, not the ExtendedXMP. */ function testExtendedXMPWithWrongGUID() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); @@ -130,7 +131,7 @@ class XMPTest extends MediaWikiTestCase { * which should cause it to ignore the ExtendedXMP packet. */ function testExtendedXMPMissingPacket() { - $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $xmpPath = __DIR__ . '/../../data/xmp/'; $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); diff --git a/tests/phpunit/includes/mobile/DeviceDetectionTest.php b/tests/phpunit/includes/mobile/DeviceDetectionTest.php new file mode 100644 index 00000000..0e156532 --- /dev/null +++ b/tests/phpunit/includes/mobile/DeviceDetectionTest.php @@ -0,0 +1,40 @@ +<?php + +/** + * @group Mobile + */ + class DeviceDetectionTest extends MediaWikiTestCase { + + /** + * @dataProvider provideTestFormatName + */ + public function testFormatName( $format, $userAgent ) { + $detector = new DeviceDetection(); + $this->assertEquals( $format, $detector->detectFormatName( $userAgent ) ); + } + + public function provideTestFormatName() { + return array( + array( 'android', 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17' ), + array( 'iphone2', 'Mozilla/5.0 (ipod: U;CPU iPhone OS 2_2 like Mac OS X: es_es) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3' ), + array( 'iphone', 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3' ), + array( 'nokia', 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413' ), + array( 'palm_pre', 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0' ), + array( 'wii', 'Opera/9.00 (Nintendo Wii; U; ; 1309-9; en)' ), + array( 'operamini', 'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)' ), + array( 'operamobile', 'Opera/9.51 Beta (Microsoft Windows; PPC; Opera Mobi/1718; U; en)' ), + array( 'kindle', 'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)' ), + array( 'kindle2', 'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)' ), + array( 'capable', 'Mozilla/5.0 (X11; Linux i686; rv:2.0.1) Gecko/20100101 Firefox/4.0.1' ), + array( 'netfront', 'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2' ), + array( 'wap2', 'SonyEricssonK608i/R2L/SN356841000828910 Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + array( 'wap2', 'NokiaN73-2/3.0-630.0.2 Series60/3.0 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + array( 'psp', 'Mozilla/4.0 (PSP (PlayStation Portable); 2.00)' ), + array( 'ps3', 'Mozilla/5.0 (PLAYSTATION 3; 1.00)' ), + array( 'ie', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)' ), + array( 'ie', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)' ), + array( 'blackberry', 'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133' ), + array( 'blackberry-lt5', 'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1' ), + ); + } +} diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index 816c017a..6a6fded1 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -1,5 +1,5 @@ <?php -require_once( dirname( __FILE__ ) . '/NewParserTest.php' ); +require_once( __DIR__ . '/NewParserTest.php' ); /** * The UnitTest must be either a class that inherits from MediaWikiTestCase diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index d9b16710..69a96e66 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -186,7 +186,7 @@ class NewParserTest extends MediaWikiTestCase { if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { $image->recordUpload2( '', // archive name - 'Upload of some lame file', + 'Upload of some lame file', 'Some lame file', array( 'size' => 12345, @@ -197,7 +197,7 @@ class NewParserTest extends MediaWikiTestCase { 'mime' => 'image/jpeg', 'metadata' => serialize( array() ), 'sha1' => wfBaseConvert( '', 16, 36, 31 ), - 'fileExists' => true ), + 'fileExists' => true ), $this->db->timestamp( '20010115123500' ), $user ); } @@ -207,8 +207,8 @@ class NewParserTest extends MediaWikiTestCase { if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { $image->recordUpload2( '', // archive name - 'zomgnotcensored', - 'Borderline image', + 'zomgnotcensored', + 'Borderline image', array( 'size' => 12345, 'width' => 320, @@ -218,7 +218,7 @@ class NewParserTest extends MediaWikiTestCase { 'mime' => 'image/jpeg', 'metadata' => serialize( array() ), 'sha1' => wfBaseConvert( '', 16, 36, 31 ), - 'fileExists' => true ), + 'fileExists' => true ), $this->db->timestamp( '20010115123500' ), $user ); } @@ -326,7 +326,6 @@ class NewParserTest extends MediaWikiTestCase { 'wgExternalLinkTarget' => false, 'wgAlwaysUseTidy' => false, 'wgHtml5' => true, - 'wgCleanupPresentationalAttributes' => true, 'wgWellFormedXml' => true, 'wgAllowMicrodataAttributes' => true, 'wgAdaptiveMessageCache' => true, @@ -345,6 +344,9 @@ class NewParserTest extends MediaWikiTestCase { $this->savedGlobals = array(); + /** @since 1.20 */ + wfRunHooks( 'ParserTestGlobals', array( &$settings ) ); + foreach ( $settings as $var => $val ) { if ( array_key_exists( $var, $GLOBALS ) ) { $this->savedGlobals[$var] = $GLOBALS[$var]; @@ -380,7 +382,7 @@ class NewParserTest extends MediaWikiTestCase { # The entries saved into RepoGroup cache with previous globals will be wrong. RepoGroup::destroySingleton(); FileBackendGroup::destroySingleton(); - MessageCache::singleton()->destroyInstance(); + MessageCache::destroyInstance(); return $context; } @@ -596,7 +598,7 @@ class NewParserTest extends MediaWikiTestCase { * Run a fuzz test series * Draw input from a set of test files * - * @todo @fixme Needs some work to not eat memory until the world explodes + * @todo fixme Needs some work to not eat memory until the world explodes * * @group ParserFuzz */ diff --git a/tests/phpunit/includes/parser/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php new file mode 100644 index 00000000..dea406c3 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserMethodsTest.php @@ -0,0 +1,33 @@ +<?php + +class ParserMethodsTest extends MediaWikiLangTestCase { + + public 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 <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgParser; + + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $user = new User(); + $user->setName( "127.0.0.1" ); + $popts = ParserOptions::newFromUser( $user ); + $text = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + $this->assertEquals( $expected, $text ); + } + + // TODO: Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText() +} + diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php index 9d3499a0..fee56748 100644 --- a/tests/phpunit/includes/parser/PreprocessorTest.php +++ b/tests/phpunit/includes/parser/PreprocessorTest.php @@ -103,7 +103,7 @@ class PreprocessorTest extends MediaWikiTestCase { array( "{{foo|bar=|}", "<root>{{foo|bar=|}</root>"), array( "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>"), array( "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>"), - /* array( file_get_contents( dirname( __FILE__ ) . '/QuoteQuran.txt' ), file_get_contents( dirname( __FILE__ ) . '/QuoteQuranExpanded.txt' ) ), */ + /* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */ ); } @@ -165,7 +165,7 @@ class PreprocessorTest extends MediaWikiTestCase { * @dataProvider provideFiles */ function testPreprocessorOutputFiles( $filename ) { - $folder = dirname( __FILE__ ) . "/../../../parser/preprocess"; + $folder = __DIR__ . "/../../../parser/preprocess"; $wikiText = file_get_contents( "$folder/$filename.txt" ); $output = $this->preprocessToXml( $wikiText ); diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php index ea9d5533..20e42a68 100644 --- a/tests/phpunit/includes/specials/SpecialSearchTest.php +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -87,6 +87,14 @@ class SpecialSearchTest extends MediaWikiTestCase { 'advanced', array( 2, 14 ), 'Bug 33583: search with no option should honor User search preferences' ), + array( + $EMPTY_REQUEST, array_fill_keys( array_map( function( $ns ) { + return "searchNs$ns"; + }, $defaultNS ), 0 ) + array( 'searchNs2' => 1, 'searchNs14' => 1 ), + 'advanced', array( 2, 14 ), + 'Bug 33583: search with no option should honor User search preferences' + . 'and have all other namespace disabled' + ), ); } diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php index d56cce31..f66c387b 100644 --- a/tests/phpunit/includes/upload/UploadFromUrlTest.php +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -20,7 +20,7 @@ class UploadFromUrlTest extends ApiTestCase { } } - protected function doApiRequest( $params, $unused = null, $appendModule = false, $user = null ) { + protected function doApiRequest( Array $params, Array $unused = null, $appendModule = false, User $user = null ) { $sessionId = session_id(); session_write_close(); @@ -228,11 +228,11 @@ class UploadFromUrlTest extends ApiTestCase { $talk = $this->user->user->getTalkPage(); if ( $talk->exists() ) { - $a = new Article( $talk ); - $a->doDeleteArticle( '' ); + $page = WikiPage::factory( $talk ); + $page->doDeleteArticle( '' ); } - $this->assertFalse( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); + $this->assertFalse( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); $data = $this->doApiRequest( array( 'action' => 'upload', @@ -249,7 +249,7 @@ class UploadFromUrlTest extends ApiTestCase { $job->run(); $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); - $this->assertTrue( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk exists' ); + $this->assertTrue( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk exists' ); $this->deleteFile( 'UploadFromUrlTest.png' ); @@ -341,8 +341,8 @@ class UploadFromUrlTest extends ApiTestCase { $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) ); $empty = ""; FileDeleteForm::doDelete( $t, $file, $empty, "none", true ); - $a = new Article ( $t ); - $a->doDeleteArticle( "testing" ); + $page = WikiPage::factory( $t ); + $page->doDeleteArticle( "testing" ); } $t = Title::newFromText( $name, NS_FILE ); diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php index c9dbb138..66fafaaf 100644 --- a/tests/phpunit/includes/upload/UploadStashTest.php +++ b/tests/phpunit/includes/upload/UploadStashTest.php @@ -12,17 +12,17 @@ class UploadStashTest extends MediaWikiTestCase { parent::setUp(); // Setup a file for bug 29408 - $this->bug29408File = dirname( __FILE__ ) . '/bug29408'; + $this->bug29408File = __DIR__ . '/bug29408'; file_put_contents( $this->bug29408File, "\x00" ); self::$users = array( - 'sysop' => new ApiTestUser( + 'sysop' => new TestUser( 'Uploadstashtestsysop', 'Upload Stash Test Sysop', 'upload_stash_test_sysop@example.com', array( 'sysop' ) ), - 'uploader' => new ApiTestUser( + 'uploader' => new TestUser( 'Uploadstashtestuser', 'Upload Stash Test User', 'upload_stash_test_user@example.com', diff --git a/tests/phpunit/includes/upload/UploadTest.php b/tests/phpunit/includes/upload/UploadTest.php index 4293d23b..6948f5b1 100644 --- a/tests/phpunit/includes/upload/UploadTest.php +++ b/tests/phpunit/includes/upload/UploadTest.php @@ -12,7 +12,9 @@ class UploadTest extends MediaWikiTestCase { $this->upload = new UploadTestHandler; $this->hooks = $wgHooks; - $wgHooks['InterwikiLoadPrefix'][] = 'MediaWikiTestCase::disableInterwikis'; + $wgHooks['InterwikiLoadPrefix'][] = function( $prefix, &$data ) { + return false; + }; } function tearDown() { diff --git a/tests/phpunit/languages/LanguageHeTest.php b/tests/phpunit/languages/LanguageHeTest.php index 9ac0f952..7833da71 100644 --- a/tests/phpunit/languages/LanguageHeTest.php +++ b/tests/phpunit/languages/LanguageHeTest.php @@ -18,31 +18,31 @@ class LanguageHeTest extends MediaWikiTestCase { /** @dataProvider providerPluralDual */ function testPluralDual( $result, $value ) { - $forms = array( 'one', 'many', 'two' ); + $forms = array( 'one', 'two', 'other' ); $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) ); } function providerPluralDual() { return array ( - array( 'many', 0 ), // Zero -> plural + array( 'other', 0 ), // Zero -> plural array( 'one', 1 ), // Singular array( 'two', 2 ), // Dual - array( 'many', 3 ), // Plural + array( 'other', 3 ), // Plural ); } /** @dataProvider providerPlural */ function testPlural( $result, $value ) { - $forms = array( 'one', 'many' ); + $forms = array( 'one', 'other' ); $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) ); } function providerPlural() { return array ( - array( 'many', 0 ), // Zero -> plural + array( 'other', 0 ), // Zero -> plural array( 'one', 1 ), // Singular - array( 'many', 2 ), // Plural, no dual provided - array( 'many', 3 ), // Plural + array( 'other', 2 ), // Plural, no dual provided + array( 'other', 3 ), // Plural ); } } diff --git a/tests/phpunit/languages/LanguageHuTest.php b/tests/phpunit/languages/LanguageHuTest.php new file mode 100644 index 00000000..adbd37ec --- /dev/null +++ b/tests/phpunit/languages/LanguageHuTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageHu.php */ +class LanguageHuTest extends MediaWikiTestCase { + private $lang; + + function setUp() { + $this->lang = Language::factory( 'Hu' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** @dataProvider providePlural */ + function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->lang->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/LanguageSrTest.php b/tests/phpunit/languages/LanguageSrTest.php index a50547c6..d44ecf8e 100644 --- a/tests/phpunit/languages/LanguageSrTest.php +++ b/tests/phpunit/languages/LanguageSrTest.php @@ -12,9 +12,9 @@ * @file */ -require_once dirname( dirname( __FILE__ ) ) . '/bootstrap.php'; +require_once dirname( __DIR__ ) . '/bootstrap.php'; -/** Tests for MediaWiki languages/LanguageTr.php */ +/** Tests for MediaWiki languages/LanguageSr.php */ class LanguageSrTest extends MediaWikiTestCase { /* Language object. Initialized before each test */ private $lang; @@ -65,18 +65,38 @@ class LanguageSrTest extends MediaWikiTestCase { * @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 šđžčć' ) @@ -84,15 +104,19 @@ class LanguageSrTest extends MediaWikiTestCase { } 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( 'абцдšđžčć' ) ); diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php index cb4c641d..95d8fde4 100644 --- a/tests/phpunit/languages/LanguageTest.php +++ b/tests/phpunit/languages/LanguageTest.php @@ -24,43 +24,195 @@ class LanguageTest extends MediaWikiTestCase { ); } - /** @dataProvider provideFormattableTimes */ + /** + * @dataProvider provideFormattableTimes + */ function testFormatTimePeriod( $seconds, $format, $expected, $desc ) { $this->assertEquals( $expected, $this->lang->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)' ), + 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)' + ), ); } @@ -98,8 +250,8 @@ class LanguageTest extends MediaWikiTestCase { } /** - * @dataProvider provideHTMLTruncateData() - */ + * @dataProvider provideHTMLTruncateData() + */ function testTruncateHtml( $len, $ellipsis, $input, $expected ) { // Actual HTML... $this->assertEquals( @@ -654,4 +806,264 @@ class LanguageTest extends MediaWikiTestCase { ), ); } + + + + /** + * @dataProvider provideFormatDuration + */ + function testFormatDuration( $duration, $expected, $intervals = array() ) { + $this->assertEquals( + $expected, + $this->lang->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.25 * 86400, // 365.25 * 86400 = 31557600 + '1 year', + ), + array( + 2 * 31557600, + '2 years', + ), + array( + 10 * 31557600, + '1 decade', + ), + array( + 20 * 31557600, + '2 decades', + ), + array( + 100 * 31557600, + '1 century', + ), + array( + 200 * 31557600, + '2 centuries', + ), + array( + 1000 * 31557600, + '1 millennium', + ), + array( + 2000 * 31557600, + '2 millennia', + ), + array( + 9001, + '2 hours, 30 minutes and 1 second' + ), + array( + 3601, + '1 hour and 1 second' + ), + array( + 31557600 + 2 * 86400 + 9000, + '1 year, 2 days, 2 hours and 30 minutes' + ), + array( + 42 * 1000 * 31557600 + 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( + 31557600 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'years', 'days', 'minutes' ), + ), + array( + 42, + '0 days', + array( 'years', 'days' ), + ), + array( + 31557600 + 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->lang->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' ), + ); + } } + diff --git a/tests/phpunit/languages/LanguageUzTest.php b/tests/phpunit/languages/LanguageUzTest.php new file mode 100644 index 00000000..72387283 --- /dev/null +++ b/tests/phpunit/languages/LanguageUzTest.php @@ -0,0 +1,120 @@ +<?php +/** + * PHPUnit tests for the Uzbek language. + * The language can be represented using two scripts: + * - Latin (uz-latn) + * - Cyrillic (uz-cyrl) + * + * @author Robin Pepermans + * @author Antoine Musso <hashar at free dot fr> + * @copyright Copyright © 2012, Robin Pepermans + * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr> + * @file + */ + +require_once dirname( __DIR__ ) . '/bootstrap.php'; + +/** Tests for MediaWiki languages/LanguageUz.php */ +class LanguageUzTest extends MediaWikiTestCase { + /* Language object. Initialized before each test */ + private $lang; + + function setUp() { + $this->lang = Language::factory( 'uz' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** + * @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->lang->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/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php new file mode 100644 index 00000000..033164b0 --- /dev/null +++ b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php @@ -0,0 +1,95 @@ +<?php +/** + * @author Niklas Laxström + * @file + */ + +class CLDRPluralRuleEvaluatorTest extends MediaWikiTestCase { + /** + * @dataProvider validTestCases + */ + function testValidRules( $expected, $rules, $number, $comment ) { + $result = CLDRPluralRuleEvaluator::evaluate( $number, (array) $rules ); + $this->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..d1344389 --- /dev/null +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,352 @@ +<?php + +/** + * Base TestCase for dumps + */ +abstract class DumpTestCase extends MediaWikiLangTestCase { + + /** + * exception to be rethrown once in sound PHPUnit surrounding + * + * As the current MediaWikiTestCase::run is not robust enough to recover + * from thrown exceptions directly, we cannot throw frow within + * self::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. + * + * @var Exception|null + */ + protected $exceptionFromAddDBData = null; + + /** + * Holds the xmlreader used for analyzing an xml dump + * + * @var XMLReader|null + */ + protected $xml = null; + + /** + * Adds a revision to a page, while returning the resuting revision's id + * + * @param $page WikiPage: page to add the revision to + * @param $text string: revisions text + * @param $text string: revisions summare + * + * @throws MWExcepion + */ + protected function addRevision( Page $page, $text, $summary ) { + $status = $page->doEdit( $text, $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 + */ + public 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 </mediawiki>) + * + * @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 </mediawiki>) + * + * @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 </siteinfo> + */ + 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 </mediawiki>) + */ + 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 <mediawiki>) + * @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 </mediawiki>) + * @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 <mediawiki>...</mediawiki>) + * @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 $parentid int|false: (optional) id of the parent revision + */ + protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) { + + $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->assertTextNode( "sha1", $text_sha1 ); + + $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(); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + +} diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..4a6f08fa --- /dev/null +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,812 @@ +<?php + +// It would be great if we were able to use PHPUnit's getMockForAbstractClass +// instead of the MaintenanceFixup hack below. However, we cannot do +// without changing the visibility and without working around hacks in +// Maintenance.php +// +// For the same reason, we cannot just use FakeMaintenance. + +/** + * makes parts of the API of Maintenance that is hidden by protected visibily + * visible for testing, and makes up for a stream closing hack in Maintenance.php. + * + * This class is solely used for being able to test Maintenance right now + * without having to apply major refactorings to fix some design issues in + * Maintenance.php. Before adding more functions here, please consider whether + * this approach is correct, or a refactoring Maintenance to separate concers + * is more appropriate. + * + * Upon refactoring, keep in mind that besides the maintenance scrits themselves + * and tests right here, also at least Extension:Maintenance make use of + * Maintenance. + * + * Due to a hack in Maintenance.php using register_shutdown_function, be sure to + * finally call simulateShutdown on MaintenanceFixup instance before a test + * ends. + * + */ +class MaintenanceFixup extends Maintenance { + + // --- Making up for the register_shutdown_function hack in Maintenance.php + + /** + * The test case that generated this instance. + * + * This member is motivated by allowing the destructor to check whether or not + * the test failed, in order to avoid unnecessary nags about omitted shutdown + * simulation. + * But as it is already available, we also usi it to flagging tests as failed + * + * @var MediaWikiTestCase + */ + private $testCase; + + /** + * shutdownSimulated === true iff simulateShutdown has done it's work + * + * @var bool + */ + private $shutdownSimulated = false; + + /** + * Simulates what Maintenance wants to happen at script's end. + */ + public function simulateShutdown() { + + if ( $this->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 ) && ( ! $this->testCase->hasFailed() ) ) { + // 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 behaviour. Hence, we can only report to the + // error output :( Hopefully people read the PHPUnit output. + fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " 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 ); + } + + + /** + * 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(); + + $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 behaviour, but rather currently existing behaviour. + + + 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 ); + } + + +}
\ No newline at end of file diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..8ff85574 --- /dev/null +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,270 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; + +/** + * Tests for BaseDump + * + * @group Dump + */ +class BaseDumpTest extends MediaWikiTestCase { + + /** + * @var BaseDump the BaseDump instance used within a test. + * + * If set, this BaseDump gets automatically closed in tearDown. + */ + private $dump = null; + + protected function tearDown() { + if ( $this->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 = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.20alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + + + // An array holding the pages that are available for prefetch + $available_pages = array(); + + // Simple plain page + $available_pages[1] = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>1</id> + <revision> + <id>1</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <text xml:space="preserve">BackupDumperTestP1Text1</text> + </revision> + </page> +'; + // Page with more than one revisions. Hole in rev ids. + $available_pages[2] = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>2</id> + <revision> + <id>2</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <text xml:space="preserve">BackupDumperTestP2Text1</text> + </revision> + <revision> + <id>5</id> + <parentid>2</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text> + </revision> + </page> +'; + // Page with id higher than previous id + 1 + $available_pages[4] = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>4</id> + <revision> + <id>8</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text> + </revision> + </page> +'; + + // The common ending for all files + $tail = '</mediawiki> +'; + + // 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..a0bbadf9 --- /dev/null +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,563 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupTextPass.inc"; + +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class TextPassDumperTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4; + private static $numOfPages = 4; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + private static $numOfRevs = 8; + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + // Simple page + $title = Title::newFromText( 'BackupDumperTestP1' ); + $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' ); + $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' ); + $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 + $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; + } + + } + + public 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, "# of checkpoint files" ); + + $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->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 = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" ' + . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ ' + . 'http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.20alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + $tail = '</mediawiki> +'; + + $content = $header; + $iterations = intval( $iterations ); + for ( $i = 0; $i < $iterations; $i++ ) { + + $page1 = ' <page> + <title>BackupDumperTestP1</title> + <ns>0</ns> + <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP1Summary1</comment> + <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1> + <text id="' . $this->textId1_1 . '" bytes="23" /> + </revision> + </page> +'; + $page2 = ' <page> + <title>BackupDumperTestP2</title> + <ns>0</ns> + <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary1</comment> + <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1> + <text id="' . $this->textId2_1 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary2</comment> + <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1> + <text id="' . $this->textId2_2 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary3</comment> + <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1> + <text id="' . $this->textId2_3 . '" bytes="23" /> + </revision> + <revision> + <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id> + <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>BackupDumperTestP2Summary4 extra</comment> + <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1> + <text id="' . $this->textId2_4 . '" bytes="44" /> + </revision> + </page> +'; + // page 3 not in stub + + $page4 = ' <page> + <title>Talk:BackupDumperTestP1</title> + <ns>1</ns> + <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id> + <revision> + <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id> + <timestamp>2012-04-01T16:46:05Z</timestamp> + <contributor> + <ip>127.0.0.1</ip> + </contributor> + <comment>Talk BackupDumperTestP1 Summary1</comment> + <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1> + <text id="' . $this->textId4_1 . '" bytes="35" /> + </revision> + </page> +'; + $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..8a8dea5a --- /dev/null +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,227 @@ +<?php +/** + * Tests for log dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class BackupDumperLoggerTest extends DumpTestCase { + + + // We'll add several log entries and users for this test. The following + // variables hold the corresponding ids. + private $userId1, $userId2; + private $logId1, $logId2, $logId3; + + /** + * adds a log entry to the database. + * + * @param $type string: type of the log entry + * @param $subtype string: subtype of the log entry + * @param $user User: user that performs the logged operation + * @param $ns int: number of the namespace for the entry's target's title + * @param $title string: title of the entry's target + * @param $comment string: comment of the log entry + * @param $parameters Array: (optional) accompanying data that is attached + * to the entry + * + * @return int id of the added log entry + */ + private function addLogEntry( $type, $subtype, User $user, $ns, $title, + $comment = null, $parameters = null ) { + + $logEntry = new ManualLogEntry( $type, $subtype ); + $logEntry->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; + + // 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..925e277d --- /dev/null +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,389 @@ +<?php +/** + * Tests for page dumps of BackupDumper + * + * @group Database + * @group Dump + */ +class BackupDumperPageTest extends DumpTestCase { + + // We'll add several pages, revision and texts. The following variables hold the + // corresponding ids. + private $pageId1, $pageId2, $pageId3, $pageId4, $pageId5; + private $revId1_1, $textId1_1; + private $revId2_1, $textId2_1, $revId2_2, $textId2_2; + private $revId2_3, $textId2_3, $revId2_4, $textId2_4; + private $revId3_1, $textId3_1, $revId3_2, $textId3_2; + private $revId4_1, $textId4_1; + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + $title = Title::newFromText( 'BackupDumperTestP1' ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + $title = Title::newFromText( 'BackupDumperTestP2' ); + $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(); + + $title = Title::newFromText( 'BackupDumperTestP3' ); + $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 ;)" ); + + $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; + } + + } + + public 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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubGzip () { + // 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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $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. + // + $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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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, NS_TALK, "Talk:BackupDumperTestP1" ); + $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, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $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 NS_MAIN. 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..e7ffa01c --- /dev/null +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,243 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/fetchText.php"; + +/** + * Mock for the input/output of FetchText + * + * FetchText internally tries to access stdin and stdout. We mock those aspects + * for testing. + */ +class SemiMockedFetchText extends FetchText { + + /** + * @var String|null Text to pass as stdin + */ + private $mockStdinText = null; + + /** + * @var bool Whether or not a text for stdin has been provided + */ + private $mockSetUp = False; + + /** + * @var Array Invocation counters for the mocked aspects + */ + private $mockInvocations = array( 'getStdin' => 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->doEdit( $text, $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'; + + try { + $title = Title::newFromText( 'FetchTextTestPage1' ); + $page = WikiPage::factory( $title ); + $this->textId1 = $this->addRevision( $page, "FetchTextTestPage1Text1", "FetchTextTestPage1Summary1" ); + + $title = Title::newFromText( 'FetchTextTestPage2' ); + $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..0b7c758c --- /dev/null +++ b/tests/phpunit/maintenance/getSlaveServerTest.php @@ -0,0 +1,69 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/getSlaveServer.php"; + +/** + * Tests for getSlaveServer + * + * @group Database + */ +class GetSlaveServerTest extends MediaWikiTestCase { + + /** + * Yields a regular expression that matches a good DB server name + * + * It matches IPs or hostnames, both optionally followed by a + * port specification + * + * @return String the regular expression + */ + private function getServerRE() { + if ( $this->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 index 92eeffa2..bcbf4ec1 100644 --- a/tests/phpunit/phpunit.php +++ b/tests/phpunit/phpunit.php @@ -9,7 +9,7 @@ /* Configuration */ // Evaluate the include path relative to this file -$IP = dirname( dirname( dirname( __FILE__ ) ) ); +$IP = dirname( dirname( __DIR__ ) ); // Set a flag which can be used to detect when other scripts have been entered through this entry point or not define( 'MW_PHPUNIT_TEST', true ); @@ -18,15 +18,31 @@ define( 'MW_PHPUNIT_TEST', true ); require_once( "$IP/maintenance/Maintenance.php" ); class PHPUnitMaintClass extends Maintenance { + + function __construct() { + parent::__construct(); + $this->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, $wgUseDatabaseMessages; + 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 @@ -35,7 +51,42 @@ class PHPUnitMaintClass extends Maintenance { $wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; } - public function execute() { } + + 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; } @@ -44,18 +95,13 @@ class PHPUnitMaintClass extends Maintenance { $maintClass = 'PHPUnitMaintClass'; require( RUN_MAINTENANCE_IF_MAIN ); -if( !in_array( '--configuration', $_SERVER['argv'] ) ) { - //Hack to eliminate the need to use the Makefile (which sucks ATM) - array_splice( $_SERVER['argv'], 1, 0, - array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) ); -} - require_once( 'PHPUnit/Runner/Version.php' ); -if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) { - die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" ); + +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/suite.xml b/tests/phpunit/suite.xml index 1227a17a..f286fa11 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -23,6 +23,11 @@ <testsuite name="skins"> <directory>skins</directory> </testsuite> + <!-- As there is a class Maintenance, we cannot use the + name "maintenance" directly --> + <testsuite name="maintenance_suite"> + <directory>maintenance</directory> + </testsuite> <testsuite name="structure"> <file>StructureTest.php</file> </testsuite> diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php index 6779ad47..f2638111 100644 --- a/tests/phpunit/suites/UploadFromUrlTestSuite.php +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -1,6 +1,6 @@ <?php -require_once( dirname( dirname( __FILE__ ) ) . '/includes/upload/UploadFromUrlTest.php' ); +require_once( dirname( __DIR__ ) . '/includes/upload/UploadFromUrlTest.php' ); class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { public $savedGlobals = array(); |