MediaWiki
REL1_22
|
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 00141 return $fname; 00142 } 00143 00152 protected function getNewTempDirectory() { 00153 // Starting of with a temporary /file/. 00154 $fname = $this->getNewTempFile(); 00155 00156 // Converting the temporary /file/ to a /directory/ 00157 // 00158 // The following is not atomic, but at least we now have a single place, 00159 // where temporary directory creation is bundled and can be improved 00160 unlink( $fname ); 00161 $this->assertTrue( wfMkdirParents( $fname ) ); 00162 00163 return $fname; 00164 } 00165 00170 protected function setUp() { 00171 wfProfileIn( __METHOD__ ); 00172 parent::setUp(); 00173 $this->called['setUp'] = 1; 00174 00175 /* 00176 // @todo global variables to restore for *every* test 00177 array( 00178 'wgLang', 00179 'wgContLang', 00180 'wgLanguageCode', 00181 'wgUser', 00182 'wgTitle', 00183 ); 00184 */ 00185 00186 // Cleaning up temporary files 00187 foreach ( $this->tmpfiles as $fname ) { 00188 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00189 unlink( $fname ); 00190 } elseif ( is_dir( $fname ) ) { 00191 wfRecursiveRemoveDir( $fname ); 00192 } 00193 } 00194 00195 if ( $this->needsDB() && $this->db ) { 00196 // Clean up open transactions 00197 while ( $this->db->trxLevel() > 0 ) { 00198 $this->db->rollback(); 00199 } 00200 00201 // don't ignore DB errors 00202 $this->db->ignoreErrors( false ); 00203 } 00204 00205 wfProfileOut( __METHOD__ ); 00206 } 00207 00208 protected function tearDown() { 00209 wfProfileIn( __METHOD__ ); 00210 00211 // Cleaning up temporary files 00212 foreach ( $this->tmpfiles as $fname ) { 00213 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00214 unlink( $fname ); 00215 } elseif ( is_dir( $fname ) ) { 00216 wfRecursiveRemoveDir( $fname ); 00217 } 00218 } 00219 00220 if ( $this->needsDB() && $this->db ) { 00221 // Clean up open transactions 00222 while ( $this->db->trxLevel() > 0 ) { 00223 $this->db->rollback(); 00224 } 00225 00226 // don't ignore DB errors 00227 $this->db->ignoreErrors( false ); 00228 } 00229 00230 // Restore mw globals 00231 foreach ( $this->mwGlobals as $key => $value ) { 00232 $GLOBALS[$key] = $value; 00233 } 00234 $this->mwGlobals = array(); 00235 00236 parent::tearDown(); 00237 wfProfileOut( __METHOD__ ); 00238 } 00239 00244 final public function testMediaWikiTestCaseParentSetupCalled() { 00245 $this->assertArrayHasKey( 'setUp', $this->called, 00246 get_called_class() . "::setUp() must call parent::setUp()" 00247 ); 00248 } 00249 00282 protected function setMwGlobals( $pairs, $value = null ) { 00283 00284 // Normalize (string, value) to an array 00285 if ( is_string( $pairs ) ) { 00286 $pairs = array( $pairs => $value ); 00287 } 00288 00289 foreach ( $pairs as $key => $value ) { 00290 // NOTE: make sure we only save the global once or a second call to 00291 // setMwGlobals() on the same global would override the original 00292 // value. 00293 if ( !array_key_exists( $key, $this->mwGlobals ) ) { 00294 $this->mwGlobals[$key] = $GLOBALS[$key]; 00295 } 00296 00297 // Override the global 00298 $GLOBALS[$key] = $value; 00299 } 00300 } 00301 00312 protected function mergeMwGlobalArrayValue( $name, $values ) { 00313 if ( !isset( $GLOBALS[$name] ) ) { 00314 $merged = $values; 00315 } else { 00316 if ( !is_array( $GLOBALS[$name] ) ) { 00317 throw new MWException( "MW global $name is not an array." ); 00318 } 00319 00320 // NOTE: do not use array_merge, it screws up for numeric keys. 00321 $merged = $GLOBALS[$name]; 00322 foreach ( $values as $k => $v ) { 00323 $merged[$k] = $v; 00324 } 00325 } 00326 00327 $this->setMwGlobals( $name, $merged ); 00328 } 00329 00330 function dbPrefix() { 00331 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; 00332 } 00333 00334 function needsDB() { 00335 # if the test says it uses database tables, it needs the database 00336 if ( $this->tablesUsed ) { 00337 return true; 00338 } 00339 00340 # if the test says it belongs to the Database group, it needs the database 00341 $rc = new ReflectionClass( $this ); 00342 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { 00343 return true; 00344 } 00345 00346 return false; 00347 } 00348 00353 function addDBData() { 00354 } 00355 00356 private function addCoreDBData() { 00357 # disabled for performance 00358 #$this->tablesUsed[] = 'page'; 00359 #$this->tablesUsed[] = 'revision'; 00360 00361 if ( $this->db->getType() == 'oracle' ) { 00362 00363 # Insert 0 user to prevent FK violations 00364 # Anonymous user 00365 $this->db->insert( 'user', array( 00366 'user_id' => 0, 00367 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); 00368 00369 # Insert 0 page to prevent FK violations 00370 # Blank page 00371 $this->db->insert( 'page', array( 00372 'page_id' => 0, 00373 'page_namespace' => 0, 00374 'page_title' => ' ', 00375 'page_restrictions' => null, 00376 'page_counter' => 0, 00377 'page_is_redirect' => 0, 00378 'page_is_new' => 0, 00379 'page_random' => 0, 00380 'page_touched' => $this->db->timestamp(), 00381 'page_latest' => 0, 00382 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); 00383 } 00384 00385 User::resetIdByNameCache(); 00386 00387 //Make sysop user 00388 $user = User::newFromName( 'UTSysop' ); 00389 00390 if ( $user->idForName() == 0 ) { 00391 $user->addToDatabase(); 00392 $user->setPassword( 'UTSysopPassword' ); 00393 00394 $user->addGroup( 'sysop' ); 00395 $user->addGroup( 'bureaucrat' ); 00396 $user->saveSettings(); 00397 } 00398 00399 //Make 1 page with 1 revision 00400 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); 00401 if ( !$page->getId() == 0 ) { 00402 $page->doEditContent( 00403 new WikitextContent( 'UTContent' ), 00404 'UTPageSummary', 00405 EDIT_NEW, 00406 false, 00407 User::newFromName( 'UTSysop' ) ); 00408 } 00409 } 00410 00416 public static function teardownTestDB() { 00417 if ( !self::$dbSetup ) { 00418 return; 00419 } 00420 00421 CloneDatabase::changePrefix( self::$oldTablePrefix ); 00422 00423 self::$oldTablePrefix = false; 00424 self::$dbSetup = false; 00425 } 00426 00446 public static function setupTestDB( DatabaseBase $db, $prefix ) { 00447 global $wgDBprefix; 00448 if ( $wgDBprefix === $prefix ) { 00449 throw new MWException( 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' ); 00450 } 00451 00452 if ( self::$dbSetup ) { 00453 return; 00454 } 00455 00456 $tablesCloned = self::listTables( $db ); 00457 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); 00458 $dbClone->useTemporaryTables( self::$useTemporaryTables ); 00459 00460 self::$dbSetup = true; 00461 self::$oldTablePrefix = $wgDBprefix; 00462 00463 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { 00464 CloneDatabase::changePrefix( $prefix ); 00465 00466 return; 00467 } else { 00468 $dbClone->cloneTableStructure(); 00469 } 00470 00471 if ( $db->getType() == 'oracle' ) { 00472 $db->query( 'BEGIN FILL_WIKI_INFO; END;' ); 00473 } 00474 } 00475 00479 private function resetDB() { 00480 if ( $this->db ) { 00481 if ( $this->db->getType() == 'oracle' ) { 00482 if ( self::$useTemporaryTables ) { 00483 wfGetLB()->closeAll(); 00484 $this->db = wfGetDB( DB_MASTER ); 00485 } else { 00486 foreach ( $this->tablesUsed as $tbl ) { 00487 if ( $tbl == 'interwiki' ) { 00488 continue; 00489 } 00490 $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ ); 00491 } 00492 } 00493 } else { 00494 foreach ( $this->tablesUsed as $tbl ) { 00495 if ( $tbl == 'interwiki' || $tbl == 'user' ) { 00496 continue; 00497 } 00498 $this->db->delete( $tbl, '*', __METHOD__ ); 00499 } 00500 } 00501 } 00502 } 00503 00504 function __call( $func, $args ) { 00505 static $compatibility = array( 00506 'assertInternalType' => 'assertType', 00507 'assertNotInternalType' => 'assertNotType', 00508 'assertInstanceOf' => 'assertType', 00509 'assertEmpty' => 'assertEmpty2', 00510 ); 00511 00512 if ( method_exists( $this->suite, $func ) ) { 00513 return call_user_func_array( array( $this->suite, $func ), $args ); 00514 } elseif ( isset( $compatibility[$func] ) ) { 00515 return call_user_func_array( array( $this, $compatibility[$func] ), $args ); 00516 } else { 00517 throw new MWException( "Called non-existant $func method on " 00518 . get_class( $this ) ); 00519 } 00520 } 00521 00522 private function assertEmpty2( $value, $msg ) { 00523 return $this->assertTrue( $value == '', $msg ); 00524 } 00525 00526 private static function unprefixTable( $tableName ) { 00527 global $wgDBprefix; 00528 00529 return substr( $tableName, strlen( $wgDBprefix ) ); 00530 } 00531 00532 private static function isNotUnittest( $table ) { 00533 return strpos( $table, 'unittest_' ) !== 0; 00534 } 00535 00536 public static function listTables( $db ) { 00537 global $wgDBprefix; 00538 00539 $tables = $db->listTables( $wgDBprefix, __METHOD__ ); 00540 00541 if ( $db->getType() === 'mysql' ) { 00542 # bug 43571: cannot clone VIEWs under MySQL 00543 $views = $db->listViews( $wgDBprefix, __METHOD__ ); 00544 $tables = array_diff( $tables, $views ); 00545 } 00546 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); 00547 00548 // Don't duplicate test tables from the previous fataled run 00549 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); 00550 00551 if ( $db->getType() == 'sqlite' ) { 00552 $tables = array_flip( $tables ); 00553 // these are subtables of searchindex and don't need to be duped/dropped separately 00554 unset( $tables['searchindex_content'] ); 00555 unset( $tables['searchindex_segdir'] ); 00556 unset( $tables['searchindex_segments'] ); 00557 $tables = array_flip( $tables ); 00558 } 00559 00560 return $tables; 00561 } 00562 00563 protected function checkDbIsSupported() { 00564 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) { 00565 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); 00566 } 00567 } 00568 00569 public function getCliArg( $offset ) { 00570 00571 if ( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { 00572 return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; 00573 } 00574 } 00575 00576 public function setCliArg( $offset, $value ) { 00577 00578 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; 00579 } 00580 00587 function hideDeprecated( $function ) { 00588 wfSuppressWarnings(); 00589 wfDeprecated( $function ); 00590 wfRestoreWarnings(); 00591 } 00592 00611 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) { 00612 if ( !$this->needsDB() ) { 00613 throw new MWException( 'When testing database state, the test cases\'s needDB()' . 00614 ' method should return true. Use @group Database or $this->tablesUsed.' ); 00615 } 00616 00617 $db = wfGetDB( DB_SLAVE ); 00618 00619 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); 00620 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); 00621 00622 $i = 0; 00623 00624 foreach ( $expectedRows as $expected ) { 00625 $r = $res->fetchRow(); 00626 self::stripStringKeys( $r ); 00627 00628 $i += 1; 00629 $this->assertNotEmpty( $r, "row #$i missing" ); 00630 00631 $this->assertEquals( $expected, $r, "row #$i mismatches" ); 00632 } 00633 00634 $r = $res->fetchRow(); 00635 self::stripStringKeys( $r ); 00636 00637 $this->assertFalse( $r, "found extra row (after #$i)" ); 00638 } 00639 00651 protected function arrayWrap( array $elements ) { 00652 return array_map( 00653 function ( $element ) { 00654 return array( $element ); 00655 }, 00656 $elements 00657 ); 00658 } 00659 00672 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { 00673 if ( !$ordered ) { 00674 $this->objectAssociativeSort( $expected ); 00675 $this->objectAssociativeSort( $actual ); 00676 } 00677 00678 if ( !$named ) { 00679 $expected = array_values( $expected ); 00680 $actual = array_values( $actual ); 00681 } 00682 00683 call_user_func_array( 00684 array( $this, 'assertEquals' ), 00685 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) 00686 ); 00687 } 00688 00701 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) { 00702 $expected = str_replace( '>', ">\n", $expected ); 00703 $actual = str_replace( '>', ">\n", $actual ); 00704 00705 $this->assertEquals( $expected, $actual, $msg ); 00706 } 00707 00715 protected function objectAssociativeSort( array &$array ) { 00716 uasort( 00717 $array, 00718 function ( $a, $b ) { 00719 return serialize( $a ) > serialize( $b ) ? 1 : -1; 00720 } 00721 ); 00722 } 00723 00733 protected static function stripStringKeys( &$r ) { 00734 if ( !is_array( $r ) ) { 00735 return; 00736 } 00737 00738 foreach ( $r as $k => $v ) { 00739 if ( is_string( $k ) ) { 00740 unset( $r[$k] ); 00741 } 00742 } 00743 } 00744 00758 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { 00759 if ( $actual === $value ) { 00760 $this->assertTrue( true, $message ); 00761 } else { 00762 $this->assertType( $type, $actual, $message ); 00763 } 00764 } 00765 00777 protected function assertType( $type, $actual, $message = '' ) { 00778 if ( class_exists( $type ) || interface_exists( $type ) ) { 00779 $this->assertInstanceOf( $type, $actual, $message ); 00780 } else { 00781 $this->assertInternalType( $type, $actual, $message ); 00782 } 00783 } 00784 00794 protected function isWikitextNS( $ns ) { 00795 global $wgNamespaceContentModels; 00796 00797 if ( isset( $wgNamespaceContentModels[$ns] ) ) { 00798 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT; 00799 } 00800 00801 return true; 00802 } 00803 00811 protected function getDefaultWikitextNS() { 00812 global $wgNamespaceContentModels; 00813 00814 static $wikitextNS = null; // this is not going to change 00815 if ( $wikitextNS !== null ) { 00816 return $wikitextNS; 00817 } 00818 00819 // quickly short out on most common case: 00820 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) { 00821 return NS_MAIN; 00822 } 00823 00824 // NOTE: prefer content namespaces 00825 $namespaces = array_unique( array_merge( 00826 MWNamespace::getContentNamespaces(), 00827 array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these 00828 MWNamespace::getValidNamespaces() 00829 ) ); 00830 00831 $namespaces = array_diff( $namespaces, array( 00832 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces 00833 ) ); 00834 00835 $talk = array_filter( $namespaces, function ( $ns ) { 00836 return MWNamespace::isTalk( $ns ); 00837 } ); 00838 00839 // prefer non-talk pages 00840 $namespaces = array_diff( $namespaces, $talk ); 00841 $namespaces = array_merge( $namespaces, $talk ); 00842 00843 // check default content model of each namespace 00844 foreach ( $namespaces as $ns ) { 00845 if ( !isset( $wgNamespaceContentModels[$ns] ) || 00846 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT 00847 ) { 00848 00849 $wikitextNS = $ns; 00850 00851 return $wikitextNS; 00852 } 00853 } 00854 00855 // give up 00856 // @todo Inside a test, we could skip the test as incomplete. 00857 // But frequently, this is used in fixture setup. 00858 throw new MWException( "No namespace defaults to wikitext!" ); 00859 } 00860 00867 protected function checkHasDiff3() { 00868 global $wgDiff3; 00869 00870 # This check may also protect against code injection in 00871 # case of broken installations. 00872 wfSuppressWarnings(); 00873 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); 00874 wfRestoreWarnings(); 00875 00876 if ( !$haveDiff3 ) { 00877 $this->markTestSkipped( "Skip test, since diff3 is not configured" ); 00878 } 00879 } 00880 00891 protected function checkHasGzip() { 00892 static $haveGzip; 00893 00894 if ( $haveGzip === null ) { 00895 $retval = null; 00896 wfShellExec( 'gzip -V', $retval ); 00897 $haveGzip = ( $retval === 0 ); 00898 } 00899 00900 if ( !$haveGzip ) { 00901 $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); 00902 } 00903 00904 return $haveGzip; 00905 } 00906 00913 protected function checkPHPExtension( $extName ) { 00914 $loaded = extension_loaded( $extName ); 00915 if ( !$loaded ) { 00916 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." ); 00917 } 00918 00919 return $loaded; 00920 } 00921 00933 protected function assertException( $code, $expected = 'Exception', $message = '' ) { 00934 $pokemons = null; 00935 00936 try { 00937 call_user_func( $code ); 00938 } catch ( Exception $pokemons ) { 00939 // Gotta Catch 'Em All! 00940 } 00941 00942 if ( $message === '' ) { 00943 $message = 'An exception of type "' . $expected . '" should have been thrown'; 00944 } 00945 00946 $this->assertInstanceOf( $expected, $pokemons, $message ); 00947 } 00948 }