MediaWiki  REL1_24
MediaWikiTestCase.php
Go to the documentation of this file.
00001 <?php
00002 
00006 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
00019     private $called = array();
00020 
00025     public static $users;
00026 
00031     protected $db;
00032 
00037     protected $tablesUsed = array(); // tables with data
00038 
00039     private static $useTemporaryTables = true;
00040     private static $reuseDB = false;
00041     private static $dbSetup = false;
00042     private static $oldTablePrefix = false;
00043 
00049     private $phpErrorLevel;
00050 
00057     private $tmpFiles = array();
00058 
00065     private $mwGlobals = array();
00066 
00070     const DB_PREFIX = 'unittest_';
00071     const ORA_DB_PREFIX = 'ut_';
00072 
00077     protected $supportedDBs = array(
00078         'mysql',
00079         'sqlite',
00080         'postgres',
00081         'oracle'
00082     );
00083 
00084     public function __construct( $name = null, array $data = array(), $dataName = '' ) {
00085         parent::__construct( $name, $data, $dataName );
00086 
00087         $this->backupGlobals = false;
00088         $this->backupStaticAttributes = false;
00089     }
00090 
00091     public function __destruct() {
00092         // Complain if self::setUp() was called, but not self::tearDown()
00093         // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
00094         if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
00095             throw new MWException( get_called_class() . "::tearDown() must call parent::tearDown()" );
00096         }
00097     }
00098 
00099     public function run( PHPUnit_Framework_TestResult $result = null ) {
00100         /* Some functions require some kind of caching, and will end up using the db,
00101          * which we can't allow, as that would open a new connection for mysql.
00102          * Replace with a HashBag. They would not be going to persist anyway.
00103          */
00104         ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
00105 
00106         $needsResetDB = false;
00107         $logName = get_class( $this ) . '::' . $this->getName( false );
00108 
00109         if ( $this->needsDB() ) {
00110             // set up a DB connection for this test to use
00111 
00112             self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
00113             self::$reuseDB = $this->getCliArg( 'reuse-db' );
00114 
00115             $this->db = wfGetDB( DB_MASTER );
00116 
00117             $this->checkDbIsSupported();
00118 
00119             if ( !self::$dbSetup ) {
00120                 wfProfileIn( $logName . ' (clone-db)' );
00121 
00122                 // switch to a temporary clone of the database
00123                 self::setupTestDB( $this->db, $this->dbPrefix() );
00124 
00125                 if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
00126                     $this->resetDB();
00127                 }
00128 
00129                 wfProfileOut( $logName . ' (clone-db)' );
00130             }
00131 
00132             wfProfileIn( $logName . ' (prepare-db)' );
00133             $this->addCoreDBData();
00134             $this->addDBData();
00135             wfProfileOut( $logName . ' (prepare-db)' );
00136 
00137             $needsResetDB = true;
00138         }
00139 
00140         wfProfileIn( $logName );
00141         parent::run( $result );
00142         wfProfileOut( $logName );
00143 
00144         if ( $needsResetDB ) {
00145             wfProfileIn( $logName . ' (reset-db)' );
00146             $this->resetDB();
00147             wfProfileOut( $logName . ' (reset-db)' );
00148         }
00149     }
00150 
00156     public function usesTemporaryTables() {
00157         return self::$useTemporaryTables;
00158     }
00159 
00169     protected function getNewTempFile() {
00170         $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
00171         $this->tmpFiles[] = $fileName;
00172 
00173         return $fileName;
00174     }
00175 
00186     protected function getNewTempDirectory() {
00187         // Starting of with a temporary /file/.
00188         $fileName = $this->getNewTempFile();
00189 
00190         // Converting the temporary /file/ to a /directory/
00191         //
00192         // The following is not atomic, but at least we now have a single place,
00193         // where temporary directory creation is bundled and can be improved
00194         unlink( $fileName );
00195         $this->assertTrue( wfMkdirParents( $fileName ) );
00196 
00197         return $fileName;
00198     }
00199 
00200     protected function setUp() {
00201         wfProfileIn( __METHOD__ );
00202         parent::setUp();
00203         $this->called['setUp'] = true;
00204 
00205         $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
00206 
00207         // Cleaning up temporary files
00208         foreach ( $this->tmpFiles as $fileName ) {
00209             if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
00210                 unlink( $fileName );
00211             } elseif ( is_dir( $fileName ) ) {
00212                 wfRecursiveRemoveDir( $fileName );
00213             }
00214         }
00215 
00216         if ( $this->needsDB() && $this->db ) {
00217             // Clean up open transactions
00218             while ( $this->db->trxLevel() > 0 ) {
00219                 $this->db->rollback();
00220             }
00221 
00222             // don't ignore DB errors
00223             $this->db->ignoreErrors( false );
00224         }
00225 
00226         wfProfileOut( __METHOD__ );
00227     }
00228 
00229     protected function tearDown() {
00230         wfProfileIn( __METHOD__ );
00231 
00232         $this->called['tearDown'] = true;
00233         // Cleaning up temporary files
00234         foreach ( $this->tmpFiles as $fileName ) {
00235             if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
00236                 unlink( $fileName );
00237             } elseif ( is_dir( $fileName ) ) {
00238                 wfRecursiveRemoveDir( $fileName );
00239             }
00240         }
00241 
00242         if ( $this->needsDB() && $this->db ) {
00243             // Clean up open transactions
00244             while ( $this->db->trxLevel() > 0 ) {
00245                 $this->db->rollback();
00246             }
00247 
00248             // don't ignore DB errors
00249             $this->db->ignoreErrors( false );
00250         }
00251 
00252         // Restore mw globals
00253         foreach ( $this->mwGlobals as $key => $value ) {
00254             $GLOBALS[$key] = $value;
00255         }
00256         $this->mwGlobals = array();
00257         RequestContext::resetMain();
00258         MediaHandler::resetCache();
00259 
00260         $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
00261 
00262         if ( $phpErrorLevel !== $this->phpErrorLevel ) {
00263             ini_set( 'error_reporting', $this->phpErrorLevel );
00264 
00265             $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
00266             $newHex = strtoupper( dechex( $phpErrorLevel ) );
00267             $message = "PHP error_reporting setting was left dirty: "
00268                 . "was 0x$oldHex before test, 0x$newHex after test!";
00269 
00270             $this->fail( $message );
00271         }
00272 
00273         parent::tearDown();
00274         wfProfileOut( __METHOD__ );
00275     }
00276 
00281     final public function testMediaWikiTestCaseParentSetupCalled() {
00282         $this->assertArrayHasKey( 'setUp', $this->called,
00283             get_called_class() . "::setUp() must call parent::setUp()"
00284         );
00285     }
00286 
00319     protected function setMwGlobals( $pairs, $value = null ) {
00320         if ( is_string( $pairs ) ) {
00321             $pairs = array( $pairs => $value );
00322         }
00323 
00324         $this->stashMwGlobals( array_keys( $pairs ) );
00325 
00326         foreach ( $pairs as $key => $value ) {
00327             $GLOBALS[$key] = $value;
00328         }
00329     }
00330 
00346     protected function stashMwGlobals( $globalKeys ) {
00347         if ( is_string( $globalKeys ) ) {
00348             $globalKeys = array( $globalKeys );
00349         }
00350 
00351         foreach ( $globalKeys as $globalKey ) {
00352             // NOTE: make sure we only save the global once or a second call to
00353             // setMwGlobals() on the same global would override the original
00354             // value.
00355             if ( !array_key_exists( $globalKey, $this->mwGlobals ) ) {
00356                 if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
00357                     throw new Exception( "Global with key {$globalKey} doesn't exist and cant be stashed" );
00358                 }
00359                 // NOTE: we serialize then unserialize the value in case it is an object
00360                 // this stops any objects being passed by reference. We could use clone
00361                 // and if is_object but this does account for objects within objects!
00362                 try {
00363                     $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
00364                 }
00365                     // NOTE; some things such as Closures are not serializable
00366                     // in this case just set the value!
00367                 catch ( Exception $e ) {
00368                     $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
00369                 }
00370             }
00371         }
00372     }
00373 
00386     protected function mergeMwGlobalArrayValue( $name, $values ) {
00387         if ( !isset( $GLOBALS[$name] ) ) {
00388             $merged = $values;
00389         } else {
00390             if ( !is_array( $GLOBALS[$name] ) ) {
00391                 throw new MWException( "MW global $name is not an array." );
00392             }
00393 
00394             // NOTE: do not use array_merge, it screws up for numeric keys.
00395             $merged = $GLOBALS[$name];
00396             foreach ( $values as $k => $v ) {
00397                 $merged[$k] = $v;
00398             }
00399         }
00400 
00401         $this->setMwGlobals( $name, $merged );
00402     }
00403 
00408     public function dbPrefix() {
00409         return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
00410     }
00411 
00416     public function needsDB() {
00417         # if the test says it uses database tables, it needs the database
00418         if ( $this->tablesUsed ) {
00419             return true;
00420         }
00421 
00422         # if the test says it belongs to the Database group, it needs the database
00423         $rc = new ReflectionClass( $this );
00424         if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
00425             return true;
00426         }
00427 
00428         return false;
00429     }
00430 
00437     public function addDBData() {
00438     }
00439 
00440     private function addCoreDBData() {
00441         if ( $this->db->getType() == 'oracle' ) {
00442 
00443             # Insert 0 user to prevent FK violations
00444             # Anonymous user
00445             $this->db->insert( 'user', array(
00446                 'user_id' => 0,
00447                 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) );
00448 
00449             # Insert 0 page to prevent FK violations
00450             # Blank page
00451             $this->db->insert( 'page', array(
00452                 'page_id' => 0,
00453                 'page_namespace' => 0,
00454                 'page_title' => ' ',
00455                 'page_restrictions' => null,
00456                 'page_counter' => 0,
00457                 'page_is_redirect' => 0,
00458                 'page_is_new' => 0,
00459                 'page_random' => 0,
00460                 'page_touched' => $this->db->timestamp(),
00461                 'page_latest' => 0,
00462                 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) );
00463         }
00464 
00465         User::resetIdByNameCache();
00466 
00467         //Make sysop user
00468         $user = User::newFromName( 'UTSysop' );
00469 
00470         if ( $user->idForName() == 0 ) {
00471             $user->addToDatabase();
00472             $user->setPassword( 'UTSysopPassword' );
00473 
00474             $user->addGroup( 'sysop' );
00475             $user->addGroup( 'bureaucrat' );
00476             $user->saveSettings();
00477         }
00478 
00479         //Make 1 page with 1 revision
00480         $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
00481         if ( $page->getId() == 0 ) {
00482             $page->doEditContent(
00483                 new WikitextContent( 'UTContent' ),
00484                 'UTPageSummary',
00485                 EDIT_NEW,
00486                 false,
00487                 User::newFromName( 'UTSysop' ) );
00488         }
00489     }
00490 
00498     public static function teardownTestDB() {
00499         if ( !self::$dbSetup ) {
00500             return;
00501         }
00502 
00503         CloneDatabase::changePrefix( self::$oldTablePrefix );
00504 
00505         self::$oldTablePrefix = false;
00506         self::$dbSetup = false;
00507     }
00508 
00530     public static function setupTestDB( DatabaseBase $db, $prefix ) {
00531         global $wgDBprefix;
00532         if ( $wgDBprefix === $prefix ) {
00533             throw new MWException(
00534                 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
00535         }
00536 
00537         if ( self::$dbSetup ) {
00538             return;
00539         }
00540 
00541         $tablesCloned = self::listTables( $db );
00542         $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
00543         $dbClone->useTemporaryTables( self::$useTemporaryTables );
00544 
00545         self::$dbSetup = true;
00546         self::$oldTablePrefix = $wgDBprefix;
00547 
00548         if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
00549             CloneDatabase::changePrefix( $prefix );
00550 
00551             return;
00552         } else {
00553             $dbClone->cloneTableStructure();
00554         }
00555 
00556         if ( $db->getType() == 'oracle' ) {
00557             $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
00558         }
00559     }
00560 
00564     private function resetDB() {
00565         if ( $this->db ) {
00566             if ( $this->db->getType() == 'oracle' ) {
00567                 if ( self::$useTemporaryTables ) {
00568                     wfGetLB()->closeAll();
00569                     $this->db = wfGetDB( DB_MASTER );
00570                 } else {
00571                     foreach ( $this->tablesUsed as $tbl ) {
00572                         if ( $tbl == 'interwiki' ) {
00573                             continue;
00574                         }
00575                         $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
00576                     }
00577                 }
00578             } else {
00579                 foreach ( $this->tablesUsed as $tbl ) {
00580                     if ( $tbl == 'interwiki' || $tbl == 'user' ) {
00581                         continue;
00582                     }
00583                     $this->db->delete( $tbl, '*', __METHOD__ );
00584                 }
00585             }
00586         }
00587     }
00588 
00598     public function __call( $func, $args ) {
00599         static $compatibility = array(
00600             'assertEmpty' => 'assertEmpty2', // assertEmpty was added in phpunit 3.7.32
00601         );
00602 
00603         if ( isset( $compatibility[$func] ) ) {
00604             return call_user_func_array( array( $this, $compatibility[$func] ), $args );
00605         } else {
00606             throw new MWException( "Called non-existant $func method on "
00607                 . get_class( $this ) );
00608         }
00609     }
00610 
00616     private function assertEmpty2( $value, $msg ) {
00617         return $this->assertTrue( $value == '', $msg );
00618     }
00619 
00620     private static function unprefixTable( $tableName ) {
00621         global $wgDBprefix;
00622 
00623         return substr( $tableName, strlen( $wgDBprefix ) );
00624     }
00625 
00626     private static function isNotUnittest( $table ) {
00627         return strpos( $table, 'unittest_' ) !== 0;
00628     }
00629 
00637     public static function listTables( $db ) {
00638         global $wgDBprefix;
00639 
00640         $tables = $db->listTables( $wgDBprefix, __METHOD__ );
00641 
00642         if ( $db->getType() === 'mysql' ) {
00643             # bug 43571: cannot clone VIEWs under MySQL
00644             $views = $db->listViews( $wgDBprefix, __METHOD__ );
00645             $tables = array_diff( $tables, $views );
00646         }
00647         $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables );
00648 
00649         // Don't duplicate test tables from the previous fataled run
00650         $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) );
00651 
00652         if ( $db->getType() == 'sqlite' ) {
00653             $tables = array_flip( $tables );
00654             // these are subtables of searchindex and don't need to be duped/dropped separately
00655             unset( $tables['searchindex_content'] );
00656             unset( $tables['searchindex_segdir'] );
00657             unset( $tables['searchindex_segments'] );
00658             $tables = array_flip( $tables );
00659         }
00660 
00661         return $tables;
00662     }
00663 
00668     protected function checkDbIsSupported() {
00669         if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
00670             throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
00671         }
00672     }
00673 
00679     public function getCliArg( $offset ) {
00680         if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
00681             return PHPUnitMaintClass::$additionalOptions[$offset];
00682         }
00683     }
00684 
00690     public function setCliArg( $offset, $value ) {
00691         PHPUnitMaintClass::$additionalOptions[$offset] = $value;
00692     }
00693 
00701     public function hideDeprecated( $function ) {
00702         wfSuppressWarnings();
00703         wfDeprecated( $function );
00704         wfRestoreWarnings();
00705     }
00706 
00725     protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
00726         if ( !$this->needsDB() ) {
00727             throw new MWException( 'When testing database state, the test cases\'s needDB()' .
00728                 ' method should return true. Use @group Database or $this->tablesUsed.' );
00729         }
00730 
00731         $db = wfGetDB( DB_SLAVE );
00732 
00733         $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) );
00734         $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
00735 
00736         $i = 0;
00737 
00738         foreach ( $expectedRows as $expected ) {
00739             $r = $res->fetchRow();
00740             self::stripStringKeys( $r );
00741 
00742             $i += 1;
00743             $this->assertNotEmpty( $r, "row #$i missing" );
00744 
00745             $this->assertEquals( $expected, $r, "row #$i mismatches" );
00746         }
00747 
00748         $r = $res->fetchRow();
00749         self::stripStringKeys( $r );
00750 
00751         $this->assertFalse( $r, "found extra row (after #$i)" );
00752     }
00753 
00765     protected function arrayWrap( array $elements ) {
00766         return array_map(
00767             function ( $element ) {
00768                 return array( $element );
00769             },
00770             $elements
00771         );
00772     }
00773 
00786     protected function assertArrayEquals( array $expected, array $actual,
00787         $ordered = false, $named = false
00788     ) {
00789         if ( !$ordered ) {
00790             $this->objectAssociativeSort( $expected );
00791             $this->objectAssociativeSort( $actual );
00792         }
00793 
00794         if ( !$named ) {
00795             $expected = array_values( $expected );
00796             $actual = array_values( $actual );
00797         }
00798 
00799         call_user_func_array(
00800             array( $this, 'assertEquals' ),
00801             array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) )
00802         );
00803     }
00804 
00817     protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
00818         $expected = str_replace( '>', ">\n", $expected );
00819         $actual = str_replace( '>', ">\n", $actual );
00820 
00821         $this->assertEquals( $expected, $actual, $msg );
00822     }
00823 
00831     protected function objectAssociativeSort( array &$array ) {
00832         uasort(
00833             $array,
00834             function ( $a, $b ) {
00835                 return serialize( $a ) > serialize( $b ) ? 1 : -1;
00836             }
00837         );
00838     }
00839 
00849     protected static function stripStringKeys( &$r ) {
00850         if ( !is_array( $r ) ) {
00851             return;
00852         }
00853 
00854         foreach ( $r as $k => $v ) {
00855             if ( is_string( $k ) ) {
00856                 unset( $r[$k] );
00857             }
00858         }
00859     }
00860 
00874     protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
00875         if ( $actual === $value ) {
00876             $this->assertTrue( true, $message );
00877         } else {
00878             $this->assertType( $type, $actual, $message );
00879         }
00880     }
00881 
00893     protected function assertType( $type, $actual, $message = '' ) {
00894         if ( class_exists( $type ) || interface_exists( $type ) ) {
00895             $this->assertInstanceOf( $type, $actual, $message );
00896         } else {
00897             $this->assertInternalType( $type, $actual, $message );
00898         }
00899     }
00900 
00910     protected function isWikitextNS( $ns ) {
00911         global $wgNamespaceContentModels;
00912 
00913         if ( isset( $wgNamespaceContentModels[$ns] ) ) {
00914             return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
00915         }
00916 
00917         return true;
00918     }
00919 
00927     protected function getDefaultWikitextNS() {
00928         global $wgNamespaceContentModels;
00929 
00930         static $wikitextNS = null; // this is not going to change
00931         if ( $wikitextNS !== null ) {
00932             return $wikitextNS;
00933         }
00934 
00935         // quickly short out on most common case:
00936         if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
00937             return NS_MAIN;
00938         }
00939 
00940         // NOTE: prefer content namespaces
00941         $namespaces = array_unique( array_merge(
00942             MWNamespace::getContentNamespaces(),
00943             array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these
00944             MWNamespace::getValidNamespaces()
00945         ) );
00946 
00947         $namespaces = array_diff( $namespaces, array(
00948             NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
00949         ) );
00950 
00951         $talk = array_filter( $namespaces, function ( $ns ) {
00952             return MWNamespace::isTalk( $ns );
00953         } );
00954 
00955         // prefer non-talk pages
00956         $namespaces = array_diff( $namespaces, $talk );
00957         $namespaces = array_merge( $namespaces, $talk );
00958 
00959         // check default content model of each namespace
00960         foreach ( $namespaces as $ns ) {
00961             if ( !isset( $wgNamespaceContentModels[$ns] ) ||
00962                 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
00963             ) {
00964 
00965                 $wikitextNS = $ns;
00966 
00967                 return $wikitextNS;
00968             }
00969         }
00970 
00971         // give up
00972         // @todo Inside a test, we could skip the test as incomplete.
00973         //        But frequently, this is used in fixture setup.
00974         throw new MWException( "No namespace defaults to wikitext!" );
00975     }
00976 
00983     protected function checkHasDiff3() {
00984         global $wgDiff3;
00985 
00986         # This check may also protect against code injection in
00987         # case of broken installations.
00988         wfSuppressWarnings();
00989         $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
00990         wfRestoreWarnings();
00991 
00992         if ( !$haveDiff3 ) {
00993             $this->markTestSkipped( "Skip test, since diff3 is not configured" );
00994         }
00995     }
00996 
01007     protected function checkHasGzip() {
01008         static $haveGzip;
01009 
01010         if ( $haveGzip === null ) {
01011             $retval = null;
01012             wfShellExec( 'gzip -V', $retval );
01013             $haveGzip = ( $retval === 0 );
01014         }
01015 
01016         if ( !$haveGzip ) {
01017             $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
01018         }
01019 
01020         return $haveGzip;
01021     }
01022 
01031     protected function checkPHPExtension( $extName ) {
01032         $loaded = extension_loaded( $extName );
01033         if ( !$loaded ) {
01034             $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
01035         }
01036 
01037         return $loaded;
01038     }
01039 
01051     protected function assertException( $code, $expected = 'Exception', $message = '' ) {
01052         $pokemons = null;
01053 
01054         try {
01055             call_user_func( $code );
01056         } catch ( Exception $pokemons ) {
01057             // Gotta Catch 'Em All!
01058         }
01059 
01060         if ( $message === '' ) {
01061             $message = 'An exception of type "' . $expected . '" should have been thrown';
01062         }
01063 
01064         $this->assertInstanceOf( $expected, $pokemons, $message );
01065     }
01066 
01081     protected function assertValidHtmlSnippet( $html ) {
01082         $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>';
01083         $this->assertValidHtmlDocument( $html );
01084     }
01085 
01097     protected function assertValidHtmlDocument( $html ) {
01098         // Note: we only validate if the tidy PHP extension is available.
01099         // In case wgTidyInternal is false, MWTidy would fall back to the command line version
01100         // of tidy. In that case however, we can not reliably detect whether a failing validation
01101         // is due to malformed HTML, or caused by tidy not being installed as a command line tool.
01102         // That would cause all HTML assertions to fail on a system that has no tidy installed.
01103         if ( !$GLOBALS['wgTidyInternal'] ) {
01104             $this->markTestSkipped( 'Tidy extension not installed' );
01105         }
01106 
01107         $errorBuffer = '';
01108         MWTidy::checkErrors( $html, $errorBuffer );
01109         $allErrors = preg_split( '/[\r\n]+/', $errorBuffer );
01110 
01111         // Filter Tidy warnings which aren't useful for us.
01112         // Tidy eg. often cries about parameters missing which have actually
01113         // been deprecated since HTML4, thus we should not care about them.
01114         $errors = preg_grep(
01115             '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m',
01116             $allErrors, PREG_GREP_INVERT
01117         );
01118 
01119         $this->assertEmpty( $errors, implode( "\n", $errors ) );
01120     }
01121 
01132     public static function assertTag( $matcher, $actual, $message = '', $isHtml = true ) {
01133         //trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED);
01134 
01135         $dom = PHPUnit_Util_XML::load( $actual, $isHtml );
01136         $tags = PHPUnit_Util_XML::findNodes( $dom, $matcher, $isHtml );
01137         $matched = count( $tags ) > 0 && $tags[0] instanceof DOMNode;
01138 
01139         self::assertTrue( $matched, $message );
01140     }
01141 }