MediaWiki
master
|
00001 <?php 00002 00003 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { 00004 public $suite; 00005 public $regex = ''; 00006 public $runDisabled = false; 00007 00011 public static $users; 00012 00016 protected $db; 00017 protected $oldTablePrefix; 00018 protected $useTemporaryTables = true; 00019 protected $reuseDB = false; 00020 protected $tablesUsed = array(); // tables with data 00021 00022 private static $dbSetup = false; 00023 00030 private $tmpfiles = array(); 00031 00038 private $mwGlobals = array(); 00039 00043 const DB_PREFIX = 'unittest_'; 00044 const ORA_DB_PREFIX = 'ut_'; 00045 00046 protected $supportedDBs = array( 00047 'mysql', 00048 'sqlite', 00049 'postgres', 00050 'oracle' 00051 ); 00052 00053 function __construct( $name = null, array $data = array(), $dataName = '' ) { 00054 parent::__construct( $name, $data, $dataName ); 00055 00056 $this->backupGlobals = false; 00057 $this->backupStaticAttributes = false; 00058 } 00059 00060 function run( PHPUnit_Framework_TestResult $result = NULL ) { 00061 /* Some functions require some kind of caching, and will end up using the db, 00062 * which we can't allow, as that would open a new connection for mysql. 00063 * Replace with a HashBag. They would not be going to persist anyway. 00064 */ 00065 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; 00066 00067 if( $this->needsDB() ) { 00068 global $wgDBprefix; 00069 00070 $this->useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); 00071 $this->reuseDB = $this->getCliArg('reuse-db'); 00072 00073 $this->db = wfGetDB( DB_MASTER ); 00074 00075 $this->checkDbIsSupported(); 00076 00077 $this->oldTablePrefix = $wgDBprefix; 00078 00079 if( !self::$dbSetup ) { 00080 $this->initDB(); 00081 self::$dbSetup = true; 00082 } 00083 00084 $this->addCoreDBData(); 00085 $this->addDBData(); 00086 00087 parent::run( $result ); 00088 00089 $this->resetDB(); 00090 } else { 00091 parent::run( $result ); 00092 } 00093 } 00094 00102 protected function getNewTempFile() { 00103 $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); 00104 $this->tmpfiles[] = $fname; 00105 return $fname; 00106 } 00107 00116 protected function getNewTempDirectory() { 00117 // Starting of with a temporary /file/. 00118 $fname = $this->getNewTempFile(); 00119 00120 // Converting the temporary /file/ to a /directory/ 00121 // 00122 // The following is not atomic, but at least we now have a single place, 00123 // where temporary directory creation is bundled and can be improved 00124 unlink( $fname ); 00125 $this->assertTrue( wfMkdirParents( $fname ) ); 00126 return $fname; 00127 } 00128 00133 protected function setUp() { 00134 parent::setUp(); 00135 00136 /* 00137 //@todo: global variables to restore for *every* test 00138 array( 00139 'wgLang', 00140 'wgContLang', 00141 'wgLanguageCode', 00142 'wgUser', 00143 'wgTitle', 00144 ); 00145 */ 00146 00147 // Cleaning up temporary files 00148 foreach ( $this->tmpfiles as $fname ) { 00149 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00150 unlink( $fname ); 00151 } elseif ( is_dir( $fname ) ) { 00152 wfRecursiveRemoveDir( $fname ); 00153 } 00154 } 00155 00156 // Clean up open transactions 00157 if ( $this->needsDB() && $this->db ) { 00158 while( $this->db->trxLevel() > 0 ) { 00159 $this->db->rollback(); 00160 } 00161 } 00162 } 00163 00164 protected function tearDown() { 00165 // Cleaning up temporary files 00166 foreach ( $this->tmpfiles as $fname ) { 00167 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00168 unlink( $fname ); 00169 } elseif ( is_dir( $fname ) ) { 00170 wfRecursiveRemoveDir( $fname ); 00171 } 00172 } 00173 00174 // Clean up open transactions 00175 if ( $this->needsDB() && $this->db ) { 00176 while( $this->db->trxLevel() > 0 ) { 00177 $this->db->rollback(); 00178 } 00179 } 00180 00181 // Restore mw globals 00182 foreach ( $this->mwGlobals as $key => $value ) { 00183 $GLOBALS[$key] = $value; 00184 } 00185 $this->mwGlobals = array(); 00186 00187 parent::tearDown(); 00188 } 00189 00222 protected function setMwGlobals( $pairs, $value = null ) { 00223 00224 // Normalize (string, value) to an array 00225 if( is_string( $pairs ) ) { 00226 $pairs = array( $pairs => $value ); 00227 } 00228 00229 foreach ( $pairs as $key => $value ) { 00230 // NOTE: make sure we only save the global once or a second call to 00231 // setMwGlobals() on the same global would override the original 00232 // value. 00233 if ( !array_key_exists( $key, $this->mwGlobals ) ) { 00234 $this->mwGlobals[$key] = $GLOBALS[$key]; 00235 } 00236 00237 // Override the global 00238 $GLOBALS[$key] = $value; 00239 } 00240 } 00241 00252 protected function mergeMwGlobalArrayValue( $name, $values ) { 00253 if ( !isset( $GLOBALS[$name] ) ) { 00254 $merged = $values; 00255 } else { 00256 if ( !is_array( $GLOBALS[$name] ) ) { 00257 throw new MWException( "MW global $name is not an array." ); 00258 } 00259 00260 // NOTE: do not use array_merge, it screws up for numeric keys. 00261 $merged = $GLOBALS[$name]; 00262 foreach ( $values as $k => $v ) { 00263 $merged[$k] = $v; 00264 } 00265 } 00266 00267 $this->setMwGlobals( $name, $merged ); 00268 } 00269 00270 function dbPrefix() { 00271 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; 00272 } 00273 00274 function needsDB() { 00275 # if the test says it uses database tables, it needs the database 00276 if ( $this->tablesUsed ) { 00277 return true; 00278 } 00279 00280 # if the test says it belongs to the Database group, it needs the database 00281 $rc = new ReflectionClass( $this ); 00282 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { 00283 return true; 00284 } 00285 00286 return false; 00287 } 00288 00293 function addDBData() {} 00294 00295 private function addCoreDBData() { 00296 # disabled for performance 00297 #$this->tablesUsed[] = 'page'; 00298 #$this->tablesUsed[] = 'revision'; 00299 00300 if ( $this->db->getType() == 'oracle' ) { 00301 00302 # Insert 0 user to prevent FK violations 00303 # Anonymous user 00304 $this->db->insert( 'user', array( 00305 'user_id' => 0, 00306 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); 00307 00308 # Insert 0 page to prevent FK violations 00309 # Blank page 00310 $this->db->insert( 'page', array( 00311 'page_id' => 0, 00312 'page_namespace' => 0, 00313 'page_title' => ' ', 00314 'page_restrictions' => NULL, 00315 'page_counter' => 0, 00316 'page_is_redirect' => 0, 00317 'page_is_new' => 0, 00318 'page_random' => 0, 00319 'page_touched' => $this->db->timestamp(), 00320 'page_latest' => 0, 00321 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); 00322 00323 } 00324 00325 User::resetIdByNameCache(); 00326 00327 //Make sysop user 00328 $user = User::newFromName( 'UTSysop' ); 00329 00330 if ( $user->idForName() == 0 ) { 00331 $user->addToDatabase(); 00332 $user->setPassword( 'UTSysopPassword' ); 00333 00334 $user->addGroup( 'sysop' ); 00335 $user->addGroup( 'bureaucrat' ); 00336 $user->saveSettings(); 00337 } 00338 00339 00340 //Make 1 page with 1 revision 00341 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); 00342 if ( !$page->getId() == 0 ) { 00343 $page->doEditContent( 00344 new WikitextContent( 'UTContent' ), 00345 'UTPageSummary', 00346 EDIT_NEW, 00347 false, 00348 User::newFromName( 'UTSysop' ) ); 00349 } 00350 } 00351 00352 private function initDB() { 00353 global $wgDBprefix; 00354 if ( $wgDBprefix === $this->dbPrefix() ) { 00355 throw new MWException( 'Cannot run unit tests, the database prefix is already "unittest_"' ); 00356 } 00357 00358 $tablesCloned = $this->listTables(); 00359 $dbClone = new CloneDatabase( $this->db, $tablesCloned, $this->dbPrefix() ); 00360 $dbClone->useTemporaryTables( $this->useTemporaryTables ); 00361 00362 if ( ( $this->db->getType() == 'oracle' || !$this->useTemporaryTables ) && $this->reuseDB ) { 00363 CloneDatabase::changePrefix( $this->dbPrefix() ); 00364 $this->resetDB(); 00365 return; 00366 } else { 00367 $dbClone->cloneTableStructure(); 00368 } 00369 00370 if ( $this->db->getType() == 'oracle' ) { 00371 $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); 00372 } 00373 } 00374 00378 private function resetDB() { 00379 if( $this->db ) { 00380 if ( $this->db->getType() == 'oracle' ) { 00381 if ( $this->useTemporaryTables ) { 00382 wfGetLB()->closeAll(); 00383 $this->db = wfGetDB( DB_MASTER ); 00384 } else { 00385 foreach( $this->tablesUsed as $tbl ) { 00386 if( $tbl == 'interwiki') continue; 00387 $this->db->query( 'TRUNCATE TABLE '.$this->db->tableName($tbl), __METHOD__ ); 00388 } 00389 } 00390 } else { 00391 foreach( $this->tablesUsed as $tbl ) { 00392 if( $tbl == 'interwiki' || $tbl == 'user' ) continue; 00393 $this->db->delete( $tbl, '*', __METHOD__ ); 00394 } 00395 } 00396 } 00397 } 00398 00399 function __call( $func, $args ) { 00400 static $compatibility = array( 00401 'assertInternalType' => 'assertType', 00402 'assertNotInternalType' => 'assertNotType', 00403 'assertInstanceOf' => 'assertType', 00404 'assertEmpty' => 'assertEmpty2', 00405 ); 00406 00407 if ( method_exists( $this->suite, $func ) ) { 00408 return call_user_func_array( array( $this->suite, $func ), $args); 00409 } elseif ( isset( $compatibility[$func] ) ) { 00410 return call_user_func_array( array( $this, $compatibility[$func] ), $args); 00411 } else { 00412 throw new MWException( "Called non-existant $func method on " 00413 . get_class( $this ) ); 00414 } 00415 } 00416 00417 private function assertEmpty2( $value, $msg ) { 00418 return $this->assertTrue( $value == '', $msg ); 00419 } 00420 00421 static private function unprefixTable( $tableName ) { 00422 global $wgDBprefix; 00423 return substr( $tableName, strlen( $wgDBprefix ) ); 00424 } 00425 00426 static private function isNotUnittest( $table ) { 00427 return strpos( $table, 'unittest_' ) !== 0; 00428 } 00429 00430 protected function listTables() { 00431 global $wgDBprefix; 00432 00433 $tables = $this->db->listTables( $wgDBprefix, __METHOD__ ); 00434 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); 00435 00436 // Don't duplicate test tables from the previous fataled run 00437 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); 00438 00439 if ( $this->db->getType() == 'sqlite' ) { 00440 $tables = array_flip( $tables ); 00441 // these are subtables of searchindex and don't need to be duped/dropped separately 00442 unset( $tables['searchindex_content'] ); 00443 unset( $tables['searchindex_segdir'] ); 00444 unset( $tables['searchindex_segments'] ); 00445 $tables = array_flip( $tables ); 00446 } 00447 return $tables; 00448 } 00449 00450 protected function checkDbIsSupported() { 00451 if( !in_array( $this->db->getType(), $this->supportedDBs ) ) { 00452 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); 00453 } 00454 } 00455 00456 public function getCliArg( $offset ) { 00457 00458 if( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { 00459 return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; 00460 } 00461 00462 } 00463 00464 public function setCliArg( $offset, $value ) { 00465 00466 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; 00467 00468 } 00469 00476 function hideDeprecated( $function ) { 00477 wfSuppressWarnings(); 00478 wfDeprecated( $function ); 00479 wfRestoreWarnings(); 00480 } 00481 00500 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) { 00501 if ( !$this->needsDB() ) { 00502 throw new MWException( 'When testing database state, the test cases\'s needDB()' . 00503 ' method should return true. Use @group Database or $this->tablesUsed.'); 00504 } 00505 00506 $db = wfGetDB( DB_SLAVE ); 00507 00508 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); 00509 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); 00510 00511 $i = 0; 00512 00513 foreach ( $expectedRows as $expected ) { 00514 $r = $res->fetchRow(); 00515 self::stripStringKeys( $r ); 00516 00517 $i += 1; 00518 $this->assertNotEmpty( $r, "row #$i missing" ); 00519 00520 $this->assertEquals( $expected, $r, "row #$i mismatches" ); 00521 } 00522 00523 $r = $res->fetchRow(); 00524 self::stripStringKeys( $r ); 00525 00526 $this->assertFalse( $r, "found extra row (after #$i)" ); 00527 } 00528 00540 protected function arrayWrap( array $elements ) { 00541 return array_map( 00542 function( $element ) { 00543 return array( $element ); 00544 }, 00545 $elements 00546 ); 00547 } 00548 00561 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { 00562 if ( !$ordered ) { 00563 $this->objectAssociativeSort( $expected ); 00564 $this->objectAssociativeSort( $actual ); 00565 } 00566 00567 if ( !$named ) { 00568 $expected = array_values( $expected ); 00569 $actual = array_values( $actual ); 00570 } 00571 00572 call_user_func_array( 00573 array( $this, 'assertEquals' ), 00574 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) 00575 ); 00576 } 00577 00590 protected function assertHTMLEquals( $expected, $actual, $msg='' ) { 00591 $expected = str_replace( '>', ">\n", $expected ); 00592 $actual = str_replace( '>', ">\n", $actual ); 00593 00594 $this->assertEquals( $expected, $actual, $msg ); 00595 } 00596 00604 protected function objectAssociativeSort( array &$array ) { 00605 uasort( 00606 $array, 00607 function( $a, $b ) { 00608 return serialize( $a ) > serialize( $b ) ? 1 : -1; 00609 } 00610 ); 00611 } 00612 00622 protected static function stripStringKeys( &$r ) { 00623 if ( !is_array( $r ) ) { 00624 return; 00625 } 00626 00627 foreach ( $r as $k => $v ) { 00628 if ( is_string( $k ) ) { 00629 unset( $r[$k] ); 00630 } 00631 } 00632 } 00633 00647 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { 00648 if ( $actual === $value ) { 00649 $this->assertTrue( true, $message ); 00650 } 00651 else { 00652 $this->assertType( $type, $actual, $message ); 00653 } 00654 } 00655 00667 protected function assertType( $type, $actual, $message = '' ) { 00668 if ( class_exists( $type ) || interface_exists( $type ) ) { 00669 $this->assertInstanceOf( $type, $actual, $message ); 00670 } 00671 else { 00672 $this->assertInternalType( $type, $actual, $message ); 00673 } 00674 } 00675 00685 protected function isWikitextNS( $ns ) { 00686 global $wgNamespaceContentModels; 00687 00688 if ( isset( $wgNamespaceContentModels[$ns] ) ) { 00689 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT; 00690 } 00691 00692 return true; 00693 } 00694 00702 protected function getDefaultWikitextNS() { 00703 global $wgNamespaceContentModels; 00704 00705 static $wikitextNS = null; // this is not going to change 00706 if ( $wikitextNS !== null ) { 00707 return $wikitextNS; 00708 } 00709 00710 // quickly short out on most common case: 00711 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) { 00712 return NS_MAIN; 00713 } 00714 00715 // NOTE: prefer content namespaces 00716 $namespaces = array_unique( array_merge( 00717 MWNamespace::getContentNamespaces(), 00718 array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these 00719 MWNamespace::getValidNamespaces() 00720 ) ); 00721 00722 $namespaces = array_diff( $namespaces, array( 00723 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces 00724 )); 00725 00726 $talk = array_filter( $namespaces, function ( $ns ) { 00727 return MWNamespace::isTalk( $ns ); 00728 } ); 00729 00730 // prefer non-talk pages 00731 $namespaces = array_diff( $namespaces, $talk ); 00732 $namespaces = array_merge( $namespaces, $talk ); 00733 00734 // check default content model of each namespace 00735 foreach ( $namespaces as $ns ) { 00736 if ( !isset( $wgNamespaceContentModels[$ns] ) || 00737 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT ) { 00738 00739 $wikitextNS = $ns; 00740 return $wikitextNS; 00741 } 00742 } 00743 00744 // give up 00745 // @todo: Inside a test, we could skip the test as incomplete. 00746 // But frequently, this is used in fixture setup. 00747 throw new MWException( "No namespace defaults to wikitext!" ); 00748 } 00749 }