MediaWiki
REL1_21
|
00001 <?php 00002 00003 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { 00004 public $suite; 00005 public $regex = ''; 00006 public $runDisabled = false; 00007 00020 private $called = array(); 00021 00025 public static $users; 00026 00030 protected $db; 00031 protected $tablesUsed = array(); // tables with data 00032 00033 private static $useTemporaryTables = true; 00034 private static $reuseDB = false; 00035 private static $dbSetup = false; 00036 private static $oldTablePrefix = false; 00037 00044 private $tmpfiles = array(); 00045 00052 private $mwGlobals = array(); 00053 00057 const DB_PREFIX = 'unittest_'; 00058 const ORA_DB_PREFIX = 'ut_'; 00059 00060 protected $supportedDBs = array( 00061 'mysql', 00062 'sqlite', 00063 'postgres', 00064 'oracle' 00065 ); 00066 00067 function __construct( $name = null, array $data = array(), $dataName = '' ) { 00068 parent::__construct( $name, $data, $dataName ); 00069 00070 $this->backupGlobals = false; 00071 $this->backupStaticAttributes = false; 00072 } 00073 00074 function run( PHPUnit_Framework_TestResult $result = null ) { 00075 /* Some functions require some kind of caching, and will end up using the db, 00076 * which we can't allow, as that would open a new connection for mysql. 00077 * Replace with a HashBag. They would not be going to persist anyway. 00078 */ 00079 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; 00080 00081 $needsResetDB = false; 00082 $logName = get_class( $this ) . '::' . $this->getName( false ); 00083 00084 if ( $this->needsDB() ) { 00085 // set up a DB connection for this test to use 00086 00087 self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); 00088 self::$reuseDB = $this->getCliArg( 'reuse-db' ); 00089 00090 $this->db = wfGetDB( DB_MASTER ); 00091 00092 $this->checkDbIsSupported(); 00093 00094 if ( !self::$dbSetup ) { 00095 wfProfileIn( $logName . ' (clone-db)' ); 00096 00097 // switch to a temporary clone of the database 00098 self::setupTestDB( $this->db, $this->dbPrefix() ); 00099 00100 if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { 00101 $this->resetDB(); 00102 } 00103 00104 wfProfileOut( $logName . ' (clone-db)' ); 00105 } 00106 00107 wfProfileIn( $logName . ' (prepare-db)' ); 00108 $this->addCoreDBData(); 00109 $this->addDBData(); 00110 wfProfileOut( $logName . ' (prepare-db)' ); 00111 00112 $needsResetDB = true; 00113 } 00114 00115 wfProfileIn( $logName ); 00116 parent::run( $result ); 00117 wfProfileOut( $logName ); 00118 00119 if ( $needsResetDB ) { 00120 wfProfileIn( $logName . ' (reset-db)' ); 00121 $this->resetDB(); 00122 wfProfileOut( $logName . ' (reset-db)' ); 00123 } 00124 } 00125 00126 function usesTemporaryTables() { 00127 return self::$useTemporaryTables; 00128 } 00129 00137 protected function getNewTempFile() { 00138 $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); 00139 $this->tmpfiles[] = $fname; 00140 return $fname; 00141 } 00142 00151 protected function getNewTempDirectory() { 00152 // Starting of with a temporary /file/. 00153 $fname = $this->getNewTempFile(); 00154 00155 // Converting the temporary /file/ to a /directory/ 00156 // 00157 // The following is not atomic, but at least we now have a single place, 00158 // where temporary directory creation is bundled and can be improved 00159 unlink( $fname ); 00160 $this->assertTrue( wfMkdirParents( $fname ) ); 00161 return $fname; 00162 } 00163 00168 protected function setUp() { 00169 wfProfileIn( __METHOD__ ); 00170 parent::setUp(); 00171 $this->called['setUp'] = 1; 00172 00173 /* 00174 //@todo: global variables to restore for *every* test 00175 array( 00176 'wgLang', 00177 'wgContLang', 00178 'wgLanguageCode', 00179 'wgUser', 00180 'wgTitle', 00181 ); 00182 */ 00183 00184 // Cleaning up temporary files 00185 foreach ( $this->tmpfiles as $fname ) { 00186 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00187 unlink( $fname ); 00188 } elseif ( is_dir( $fname ) ) { 00189 wfRecursiveRemoveDir( $fname ); 00190 } 00191 } 00192 00193 if ( $this->needsDB() && $this->db ) { 00194 // Clean up open transactions 00195 while ( $this->db->trxLevel() > 0 ) { 00196 $this->db->rollback(); 00197 } 00198 00199 // don't ignore DB errors 00200 $this->db->ignoreErrors( false ); 00201 } 00202 00203 wfProfileOut( __METHOD__ ); 00204 } 00205 00206 protected function tearDown() { 00207 wfProfileIn( __METHOD__ ); 00208 00209 // Cleaning up temporary files 00210 foreach ( $this->tmpfiles as $fname ) { 00211 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00212 unlink( $fname ); 00213 } elseif ( is_dir( $fname ) ) { 00214 wfRecursiveRemoveDir( $fname ); 00215 } 00216 } 00217 00218 if ( $this->needsDB() && $this->db ) { 00219 // Clean up open transactions 00220 while ( $this->db->trxLevel() > 0 ) { 00221 $this->db->rollback(); 00222 } 00223 00224 // don't ignore DB errors 00225 $this->db->ignoreErrors( false ); 00226 } 00227 00228 // Restore mw globals 00229 foreach ( $this->mwGlobals as $key => $value ) { 00230 $GLOBALS[$key] = $value; 00231 } 00232 $this->mwGlobals = array(); 00233 00234 parent::tearDown(); 00235 wfProfileOut( __METHOD__ ); 00236 } 00237 00242 final public function testMediaWikiTestCaseParentSetupCalled() { 00243 $this->assertArrayHasKey( 'setUp', $this->called, 00244 get_called_class() . "::setUp() must call parent::setUp()" 00245 ); 00246 } 00247 00280 protected function setMwGlobals( $pairs, $value = null ) { 00281 00282 // Normalize (string, value) to an array 00283 if ( is_string( $pairs ) ) { 00284 $pairs = array( $pairs => $value ); 00285 } 00286 00287 foreach ( $pairs as $key => $value ) { 00288 // NOTE: make sure we only save the global once or a second call to 00289 // setMwGlobals() on the same global would override the original 00290 // value. 00291 if ( !array_key_exists( $key, $this->mwGlobals ) ) { 00292 $this->mwGlobals[$key] = $GLOBALS[$key]; 00293 } 00294 00295 // Override the global 00296 $GLOBALS[$key] = $value; 00297 } 00298 } 00299 00310 protected function mergeMwGlobalArrayValue( $name, $values ) { 00311 if ( !isset( $GLOBALS[$name] ) ) { 00312 $merged = $values; 00313 } else { 00314 if ( !is_array( $GLOBALS[$name] ) ) { 00315 throw new MWException( "MW global $name is not an array." ); 00316 } 00317 00318 // NOTE: do not use array_merge, it screws up for numeric keys. 00319 $merged = $GLOBALS[$name]; 00320 foreach ( $values as $k => $v ) { 00321 $merged[$k] = $v; 00322 } 00323 } 00324 00325 $this->setMwGlobals( $name, $merged ); 00326 } 00327 00328 function dbPrefix() { 00329 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; 00330 } 00331 00332 function needsDB() { 00333 # if the test says it uses database tables, it needs the database 00334 if ( $this->tablesUsed ) { 00335 return true; 00336 } 00337 00338 # if the test says it belongs to the Database group, it needs the database 00339 $rc = new ReflectionClass( $this ); 00340 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { 00341 return true; 00342 } 00343 00344 return false; 00345 } 00346 00351 function addDBData() {} 00352 00353 private function addCoreDBData() { 00354 # disabled for performance 00355 #$this->tablesUsed[] = 'page'; 00356 #$this->tablesUsed[] = 'revision'; 00357 00358 if ( $this->db->getType() == 'oracle' ) { 00359 00360 # Insert 0 user to prevent FK violations 00361 # Anonymous user 00362 $this->db->insert( 'user', array( 00363 'user_id' => 0, 00364 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); 00365 00366 # Insert 0 page to prevent FK violations 00367 # Blank page 00368 $this->db->insert( 'page', array( 00369 'page_id' => 0, 00370 'page_namespace' => 0, 00371 'page_title' => ' ', 00372 'page_restrictions' => null, 00373 'page_counter' => 0, 00374 'page_is_redirect' => 0, 00375 'page_is_new' => 0, 00376 'page_random' => 0, 00377 'page_touched' => $this->db->timestamp(), 00378 'page_latest' => 0, 00379 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); 00380 00381 } 00382 00383 User::resetIdByNameCache(); 00384 00385 //Make sysop user 00386 $user = User::newFromName( 'UTSysop' ); 00387 00388 if ( $user->idForName() == 0 ) { 00389 $user->addToDatabase(); 00390 $user->setPassword( 'UTSysopPassword' ); 00391 00392 $user->addGroup( 'sysop' ); 00393 $user->addGroup( 'bureaucrat' ); 00394 $user->saveSettings(); 00395 } 00396 00397 00398 //Make 1 page with 1 revision 00399 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); 00400 if ( !$page->getId() == 0 ) { 00401 $page->doEditContent( 00402 new WikitextContent( 'UTContent' ), 00403 'UTPageSummary', 00404 EDIT_NEW, 00405 false, 00406 User::newFromName( 'UTSysop' ) ); 00407 } 00408 } 00409 00415 public static function teardownTestDB() { 00416 if ( !self::$dbSetup ) { 00417 return; 00418 } 00419 00420 CloneDatabase::changePrefix( self::$oldTablePrefix ); 00421 00422 self::$oldTablePrefix = false; 00423 self::$dbSetup = false; 00424 } 00425 00445 public static function setupTestDB( DatabaseBase $db, $prefix ) { 00446 global $wgDBprefix; 00447 if ( $wgDBprefix === $prefix ) { 00448 throw new MWException( 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' ); 00449 } 00450 00451 if ( self::$dbSetup ) { 00452 return; 00453 } 00454 00455 $tablesCloned = self::listTables( $db ); 00456 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); 00457 $dbClone->useTemporaryTables( self::$useTemporaryTables ); 00458 00459 self::$dbSetup = true; 00460 self::$oldTablePrefix = $wgDBprefix; 00461 00462 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { 00463 CloneDatabase::changePrefix( $prefix ); 00464 return; 00465 } else { 00466 $dbClone->cloneTableStructure(); 00467 } 00468 00469 if ( $db->getType() == 'oracle' ) { 00470 $db->query( 'BEGIN FILL_WIKI_INFO; END;' ); 00471 } 00472 } 00473 00477 private function resetDB() { 00478 if ( $this->db ) { 00479 if ( $this->db->getType() == 'oracle' ) { 00480 if ( self::$useTemporaryTables ) { 00481 wfGetLB()->closeAll(); 00482 $this->db = wfGetDB( DB_MASTER ); 00483 } else { 00484 foreach ( $this->tablesUsed as $tbl ) { 00485 if ( $tbl == 'interwiki' ) { 00486 continue; 00487 } 00488 $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ ); 00489 } 00490 } 00491 } else { 00492 foreach ( $this->tablesUsed as $tbl ) { 00493 if ( $tbl == 'interwiki' || $tbl == 'user' ) { 00494 continue; 00495 } 00496 $this->db->delete( $tbl, '*', __METHOD__ ); 00497 } 00498 } 00499 } 00500 } 00501 00502 function __call( $func, $args ) { 00503 static $compatibility = array( 00504 'assertInternalType' => 'assertType', 00505 'assertNotInternalType' => 'assertNotType', 00506 'assertInstanceOf' => 'assertType', 00507 'assertEmpty' => 'assertEmpty2', 00508 ); 00509 00510 if ( method_exists( $this->suite, $func ) ) { 00511 return call_user_func_array( array( $this->suite, $func ), $args ); 00512 } elseif ( isset( $compatibility[$func] ) ) { 00513 return call_user_func_array( array( $this, $compatibility[$func] ), $args ); 00514 } else { 00515 throw new MWException( "Called non-existant $func method on " 00516 . get_class( $this ) ); 00517 } 00518 } 00519 00520 private function assertEmpty2( $value, $msg ) { 00521 return $this->assertTrue( $value == '', $msg ); 00522 } 00523 00524 private static function unprefixTable( $tableName ) { 00525 global $wgDBprefix; 00526 return substr( $tableName, strlen( $wgDBprefix ) ); 00527 } 00528 00529 private static function isNotUnittest( $table ) { 00530 return strpos( $table, 'unittest_' ) !== 0; 00531 } 00532 00533 public static function listTables( $db ) { 00534 global $wgDBprefix; 00535 00536 $tables = $db->listTables( $wgDBprefix, __METHOD__ ); 00537 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); 00538 00539 // Don't duplicate test tables from the previous fataled run 00540 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); 00541 00542 if ( $db->getType() == 'sqlite' ) { 00543 $tables = array_flip( $tables ); 00544 // these are subtables of searchindex and don't need to be duped/dropped separately 00545 unset( $tables['searchindex_content'] ); 00546 unset( $tables['searchindex_segdir'] ); 00547 unset( $tables['searchindex_segments'] ); 00548 $tables = array_flip( $tables ); 00549 } 00550 return $tables; 00551 } 00552 00553 protected function checkDbIsSupported() { 00554 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) { 00555 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); 00556 } 00557 } 00558 00559 public function getCliArg( $offset ) { 00560 00561 if ( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { 00562 return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; 00563 } 00564 00565 } 00566 00567 public function setCliArg( $offset, $value ) { 00568 00569 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; 00570 00571 } 00572 00579 function hideDeprecated( $function ) { 00580 wfSuppressWarnings(); 00581 wfDeprecated( $function ); 00582 wfRestoreWarnings(); 00583 } 00584 00603 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) { 00604 if ( !$this->needsDB() ) { 00605 throw new MWException( 'When testing database state, the test cases\'s needDB()' . 00606 ' method should return true. Use @group Database or $this->tablesUsed.' ); 00607 } 00608 00609 $db = wfGetDB( DB_SLAVE ); 00610 00611 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); 00612 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); 00613 00614 $i = 0; 00615 00616 foreach ( $expectedRows as $expected ) { 00617 $r = $res->fetchRow(); 00618 self::stripStringKeys( $r ); 00619 00620 $i += 1; 00621 $this->assertNotEmpty( $r, "row #$i missing" ); 00622 00623 $this->assertEquals( $expected, $r, "row #$i mismatches" ); 00624 } 00625 00626 $r = $res->fetchRow(); 00627 self::stripStringKeys( $r ); 00628 00629 $this->assertFalse( $r, "found extra row (after #$i)" ); 00630 } 00631 00643 protected function arrayWrap( array $elements ) { 00644 return array_map( 00645 function ( $element ) { 00646 return array( $element ); 00647 }, 00648 $elements 00649 ); 00650 } 00651 00664 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { 00665 if ( !$ordered ) { 00666 $this->objectAssociativeSort( $expected ); 00667 $this->objectAssociativeSort( $actual ); 00668 } 00669 00670 if ( !$named ) { 00671 $expected = array_values( $expected ); 00672 $actual = array_values( $actual ); 00673 } 00674 00675 call_user_func_array( 00676 array( $this, 'assertEquals' ), 00677 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) 00678 ); 00679 } 00680 00693 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) { 00694 $expected = str_replace( '>', ">\n", $expected ); 00695 $actual = str_replace( '>', ">\n", $actual ); 00696 00697 $this->assertEquals( $expected, $actual, $msg ); 00698 } 00699 00707 protected function objectAssociativeSort( array &$array ) { 00708 uasort( 00709 $array, 00710 function ( $a, $b ) { 00711 return serialize( $a ) > serialize( $b ) ? 1 : -1; 00712 } 00713 ); 00714 } 00715 00725 protected static function stripStringKeys( &$r ) { 00726 if ( !is_array( $r ) ) { 00727 return; 00728 } 00729 00730 foreach ( $r as $k => $v ) { 00731 if ( is_string( $k ) ) { 00732 unset( $r[$k] ); 00733 } 00734 } 00735 } 00736 00750 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { 00751 if ( $actual === $value ) { 00752 $this->assertTrue( true, $message ); 00753 } else { 00754 $this->assertType( $type, $actual, $message ); 00755 } 00756 } 00757 00769 protected function assertType( $type, $actual, $message = '' ) { 00770 if ( class_exists( $type ) || interface_exists( $type ) ) { 00771 $this->assertInstanceOf( $type, $actual, $message ); 00772 } else { 00773 $this->assertInternalType( $type, $actual, $message ); 00774 } 00775 } 00776 00786 protected function isWikitextNS( $ns ) { 00787 global $wgNamespaceContentModels; 00788 00789 if ( isset( $wgNamespaceContentModels[$ns] ) ) { 00790 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT; 00791 } 00792 00793 return true; 00794 } 00795 00803 protected function getDefaultWikitextNS() { 00804 global $wgNamespaceContentModels; 00805 00806 static $wikitextNS = null; // this is not going to change 00807 if ( $wikitextNS !== null ) { 00808 return $wikitextNS; 00809 } 00810 00811 // quickly short out on most common case: 00812 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) { 00813 return NS_MAIN; 00814 } 00815 00816 // NOTE: prefer content namespaces 00817 $namespaces = array_unique( array_merge( 00818 MWNamespace::getContentNamespaces(), 00819 array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these 00820 MWNamespace::getValidNamespaces() 00821 ) ); 00822 00823 $namespaces = array_diff( $namespaces, array( 00824 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces 00825 ) ); 00826 00827 $talk = array_filter( $namespaces, function ( $ns ) { 00828 return MWNamespace::isTalk( $ns ); 00829 } ); 00830 00831 // prefer non-talk pages 00832 $namespaces = array_diff( $namespaces, $talk ); 00833 $namespaces = array_merge( $namespaces, $talk ); 00834 00835 // check default content model of each namespace 00836 foreach ( $namespaces as $ns ) { 00837 if ( !isset( $wgNamespaceContentModels[$ns] ) || 00838 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT 00839 ) { 00840 00841 $wikitextNS = $ns; 00842 return $wikitextNS; 00843 } 00844 } 00845 00846 // give up 00847 // @todo: Inside a test, we could skip the test as incomplete. 00848 // But frequently, this is used in fixture setup. 00849 throw new MWException( "No namespace defaults to wikitext!" ); 00850 } 00851 00858 protected function checkHasDiff3() { 00859 global $wgDiff3; 00860 00861 # This check may also protect against code injection in 00862 # case of broken installations. 00863 wfSuppressWarnings(); 00864 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); 00865 wfRestoreWarnings(); 00866 00867 if ( !$haveDiff3 ) { 00868 $this->markTestSkipped( "Skip test, since diff3 is not configured" ); 00869 } 00870 } 00871 00882 protected function checkHasGzip() { 00883 static $haveGzip; 00884 00885 if ( $haveGzip === null ) { 00886 $retval = null; 00887 wfShellExec( 'gzip -V', $retval ); 00888 $haveGzip = ( $retval === 0 ); 00889 } 00890 00891 if ( !$haveGzip ) { 00892 $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); 00893 } 00894 00895 return $haveGzip; 00896 } 00897 00904 protected function checkPHPExtension( $extName ) { 00905 $loaded = extension_loaded( $extName ); 00906 if ( !$loaded ) { 00907 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." ); 00908 } 00909 return $loaded; 00910 } 00911 00922 protected function assertException( $code, $expected = 'Exception', $message = '' ) { 00923 $pokemons = null; 00924 00925 try { 00926 call_user_func( $code ); 00927 } catch ( Exception $pokemons ) { 00928 // Gotta Catch 'Em All! 00929 } 00930 00931 if ( $message === '' ) { 00932 $message = 'An exception of type "' . $expected . '" should have been thrown'; 00933 } 00934 00935 $this->assertInstanceOf( $expected, $pokemons, $message ); 00936 } 00937 00938 }