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