MediaWiki
REL1_20
|
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 00032 00036 const DB_PREFIX = 'unittest_'; 00037 const ORA_DB_PREFIX = 'ut_'; 00038 00039 protected $supportedDBs = array( 00040 'mysql', 00041 'sqlite', 00042 'postgres', 00043 'oracle' 00044 ); 00045 00046 function __construct( $name = null, array $data = array(), $dataName = '' ) { 00047 parent::__construct( $name, $data, $dataName ); 00048 00049 $this->backupGlobals = false; 00050 $this->backupStaticAttributes = false; 00051 } 00052 00053 function run( PHPUnit_Framework_TestResult $result = NULL ) { 00054 /* Some functions require some kind of caching, and will end up using the db, 00055 * which we can't allow, as that would open a new connection for mysql. 00056 * Replace with a HashBag. They would not be going to persist anyway. 00057 */ 00058 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; 00059 00060 if( $this->needsDB() ) { 00061 global $wgDBprefix; 00062 00063 $this->useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); 00064 $this->reuseDB = $this->getCliArg('reuse-db'); 00065 00066 $this->db = wfGetDB( DB_MASTER ); 00067 00068 $this->checkDbIsSupported(); 00069 00070 $this->oldTablePrefix = $wgDBprefix; 00071 00072 if( !self::$dbSetup ) { 00073 $this->initDB(); 00074 self::$dbSetup = true; 00075 } 00076 00077 $this->addCoreDBData(); 00078 $this->addDBData(); 00079 00080 parent::run( $result ); 00081 00082 $this->resetDB(); 00083 } else { 00084 parent::run( $result ); 00085 } 00086 } 00087 00095 protected function getNewTempFile() { 00096 $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); 00097 $this->tmpfiles[] = $fname; 00098 return $fname; 00099 } 00100 00109 protected function getNewTempDirectory() { 00110 // Starting of with a temporary /file/. 00111 $fname = $this->getNewTempFile(); 00112 00113 // Converting the temporary /file/ to a /directory/ 00114 // 00115 // The following is not atomic, but at least we now have a single place, 00116 // where temporary directory creation is bundled and can be improved 00117 unlink( $fname ); 00118 $this->assertTrue( wfMkdirParents( $fname ) ); 00119 return $fname; 00120 } 00121 00122 protected function tearDown() { 00123 // Cleaning up temporary files 00124 foreach ( $this->tmpfiles as $fname ) { 00125 if ( is_file( $fname ) || ( is_link( $fname ) ) ) { 00126 unlink( $fname ); 00127 } elseif ( is_dir( $fname ) ) { 00128 wfRecursiveRemoveDir( $fname ); 00129 } 00130 } 00131 00132 // clean up open transactions 00133 if( $this->needsDB() && $this->db ) { 00134 while( $this->db->trxLevel() > 0 ) { 00135 $this->db->rollback(); 00136 } 00137 } 00138 00139 parent::tearDown(); 00140 } 00141 00142 function dbPrefix() { 00143 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; 00144 } 00145 00146 function needsDB() { 00147 # if the test says it uses database tables, it needs the database 00148 if ( $this->tablesUsed ) { 00149 return true; 00150 } 00151 00152 # if the test says it belongs to the Database group, it needs the database 00153 $rc = new ReflectionClass( $this ); 00154 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { 00155 return true; 00156 } 00157 00158 return false; 00159 } 00160 00165 function addDBData() {} 00166 00167 private function addCoreDBData() { 00168 # disabled for performance 00169 #$this->tablesUsed[] = 'page'; 00170 #$this->tablesUsed[] = 'revision'; 00171 00172 if ( $this->db->getType() == 'oracle' ) { 00173 00174 # Insert 0 user to prevent FK violations 00175 # Anonymous user 00176 $this->db->insert( 'user', array( 00177 'user_id' => 0, 00178 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); 00179 00180 # Insert 0 page to prevent FK violations 00181 # Blank page 00182 $this->db->insert( 'page', array( 00183 'page_id' => 0, 00184 'page_namespace' => 0, 00185 'page_title' => ' ', 00186 'page_restrictions' => NULL, 00187 'page_counter' => 0, 00188 'page_is_redirect' => 0, 00189 'page_is_new' => 0, 00190 'page_random' => 0, 00191 'page_touched' => $this->db->timestamp(), 00192 'page_latest' => 0, 00193 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); 00194 00195 } 00196 00197 User::resetIdByNameCache(); 00198 00199 //Make sysop user 00200 $user = User::newFromName( 'UTSysop' ); 00201 00202 if ( $user->idForName() == 0 ) { 00203 $user->addToDatabase(); 00204 $user->setPassword( 'UTSysopPassword' ); 00205 00206 $user->addGroup( 'sysop' ); 00207 $user->addGroup( 'bureaucrat' ); 00208 $user->saveSettings(); 00209 } 00210 00211 00212 //Make 1 page with 1 revision 00213 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); 00214 if ( !$page->getId() == 0 ) { 00215 $page->doEdit( 'UTContent', 00216 'UTPageSummary', 00217 EDIT_NEW, 00218 false, 00219 User::newFromName( 'UTSysop' ) ); 00220 } 00221 } 00222 00223 private function initDB() { 00224 global $wgDBprefix; 00225 if ( $wgDBprefix === $this->dbPrefix() ) { 00226 throw new MWException( 'Cannot run unit tests, the database prefix is already "unittest_"' ); 00227 } 00228 00229 $tablesCloned = $this->listTables(); 00230 $dbClone = new CloneDatabase( $this->db, $tablesCloned, $this->dbPrefix() ); 00231 $dbClone->useTemporaryTables( $this->useTemporaryTables ); 00232 00233 if ( ( $this->db->getType() == 'oracle' || !$this->useTemporaryTables ) && $this->reuseDB ) { 00234 CloneDatabase::changePrefix( $this->dbPrefix() ); 00235 $this->resetDB(); 00236 return; 00237 } else { 00238 $dbClone->cloneTableStructure(); 00239 } 00240 00241 if ( $this->db->getType() == 'oracle' ) { 00242 $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); 00243 } 00244 } 00245 00249 private function resetDB() { 00250 if( $this->db ) { 00251 if ( $this->db->getType() == 'oracle' ) { 00252 if ( $this->useTemporaryTables ) { 00253 wfGetLB()->closeAll(); 00254 $this->db = wfGetDB( DB_MASTER ); 00255 } else { 00256 foreach( $this->tablesUsed as $tbl ) { 00257 if( $tbl == 'interwiki') continue; 00258 $this->db->query( 'TRUNCATE TABLE '.$this->db->tableName($tbl), __METHOD__ ); 00259 } 00260 } 00261 } else { 00262 foreach( $this->tablesUsed as $tbl ) { 00263 if( $tbl == 'interwiki' || $tbl == 'user' ) continue; 00264 $this->db->delete( $tbl, '*', __METHOD__ ); 00265 } 00266 } 00267 } 00268 } 00269 00270 function __call( $func, $args ) { 00271 static $compatibility = array( 00272 'assertInternalType' => 'assertType', 00273 'assertNotInternalType' => 'assertNotType', 00274 'assertInstanceOf' => 'assertType', 00275 'assertEmpty' => 'assertEmpty2', 00276 ); 00277 00278 if ( method_exists( $this->suite, $func ) ) { 00279 return call_user_func_array( array( $this->suite, $func ), $args); 00280 } elseif ( isset( $compatibility[$func] ) ) { 00281 return call_user_func_array( array( $this, $compatibility[$func] ), $args); 00282 } else { 00283 throw new MWException( "Called non-existant $func method on " 00284 . get_class( $this ) ); 00285 } 00286 } 00287 00288 private function assertEmpty2( $value, $msg ) { 00289 return $this->assertTrue( $value == '', $msg ); 00290 } 00291 00292 static private function unprefixTable( $tableName ) { 00293 global $wgDBprefix; 00294 return substr( $tableName, strlen( $wgDBprefix ) ); 00295 } 00296 00297 static private function isNotUnittest( $table ) { 00298 return strpos( $table, 'unittest_' ) !== 0; 00299 } 00300 00301 protected function listTables() { 00302 global $wgDBprefix; 00303 00304 $tables = $this->db->listTables( $wgDBprefix, __METHOD__ ); 00305 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); 00306 00307 // Don't duplicate test tables from the previous fataled run 00308 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); 00309 00310 if ( $this->db->getType() == 'sqlite' ) { 00311 $tables = array_flip( $tables ); 00312 // these are subtables of searchindex and don't need to be duped/dropped separately 00313 unset( $tables['searchindex_content'] ); 00314 unset( $tables['searchindex_segdir'] ); 00315 unset( $tables['searchindex_segments'] ); 00316 $tables = array_flip( $tables ); 00317 } 00318 return $tables; 00319 } 00320 00321 protected function checkDbIsSupported() { 00322 if( !in_array( $this->db->getType(), $this->supportedDBs ) ) { 00323 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); 00324 } 00325 } 00326 00327 public function getCliArg( $offset ) { 00328 00329 if( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { 00330 return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; 00331 } 00332 00333 } 00334 00335 public function setCliArg( $offset, $value ) { 00336 00337 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; 00338 00339 } 00340 00347 function hideDeprecated( $function ) { 00348 wfSuppressWarnings(); 00349 wfDeprecated( $function ); 00350 wfRestoreWarnings(); 00351 } 00352 00371 protected function assertSelect( $table, $fields, $condition, Array $expectedRows ) { 00372 if ( !$this->needsDB() ) { 00373 throw new MWException( 'When testing database state, the test cases\'s needDB()' . 00374 ' method should return true. Use @group Database or $this->tablesUsed.'); 00375 } 00376 00377 $db = wfGetDB( DB_SLAVE ); 00378 00379 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); 00380 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); 00381 00382 $i = 0; 00383 00384 foreach ( $expectedRows as $expected ) { 00385 $r = $res->fetchRow(); 00386 self::stripStringKeys( $r ); 00387 00388 $i += 1; 00389 $this->assertNotEmpty( $r, "row #$i missing" ); 00390 00391 $this->assertEquals( $expected, $r, "row #$i mismatches" ); 00392 } 00393 00394 $r = $res->fetchRow(); 00395 self::stripStringKeys( $r ); 00396 00397 $this->assertFalse( $r, "found extra row (after #$i)" ); 00398 } 00399 00411 protected function arrayWrap( array $elements ) { 00412 return array_map( 00413 function( $element ) { 00414 return array( $element ); 00415 }, 00416 $elements 00417 ); 00418 } 00419 00432 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { 00433 if ( !$ordered ) { 00434 $this->objectAssociativeSort( $expected ); 00435 $this->objectAssociativeSort( $actual ); 00436 } 00437 00438 if ( !$named ) { 00439 $expected = array_values( $expected ); 00440 $actual = array_values( $actual ); 00441 } 00442 00443 call_user_func_array( 00444 array( $this, 'assertEquals' ), 00445 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) 00446 ); 00447 } 00448 00461 protected function assertHTMLEquals( $expected, $actual, $msg='' ) { 00462 $expected = str_replace( '>', ">\n", $expected ); 00463 $actual = str_replace( '>', ">\n", $actual ); 00464 00465 $this->assertEquals( $expected, $actual, $msg ); 00466 } 00467 00475 protected function objectAssociativeSort( array &$array ) { 00476 uasort( 00477 $array, 00478 function( $a, $b ) { 00479 return serialize( $a ) > serialize( $b ) ? 1 : -1; 00480 } 00481 ); 00482 } 00483 00493 protected static function stripStringKeys( &$r ) { 00494 if ( !is_array( $r ) ) { 00495 return; 00496 } 00497 00498 foreach ( $r as $k => $v ) { 00499 if ( is_string( $k ) ) { 00500 unset( $r[$k] ); 00501 } 00502 } 00503 } 00504 00518 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { 00519 if ( $actual === $value ) { 00520 $this->assertTrue( true, $message ); 00521 } 00522 else { 00523 $this->assertType( $type, $actual, $message ); 00524 } 00525 } 00526 00538 protected function assertType( $type, $actual, $message = '' ) { 00539 if ( is_object( $actual ) ) { 00540 $this->assertInstanceOf( $type, $actual, $message ); 00541 } 00542 else { 00543 $this->assertInternalType( $type, $actual, $message ); 00544 } 00545 } 00546 00547 }