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