MediaWiki  master
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 
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 }