MediaWiki  REL1_20
MediaWikiTestCase.php
Go to the documentation of this file.
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 }