MediaWiki
REL1_23
|
00001 <?php 00002 00006 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { 00007 00020 private $called = array(); 00021 00026 public static $users; 00027 00032 protected $db; 00033 00038 protected $tablesUsed = array(); // tables with data 00039 00040 private static $useTemporaryTables = true; 00041 private static $reuseDB = false; 00042 private static $dbSetup = false; 00043 private static $oldTablePrefix = false; 00044 00050 private $phpErrorLevel; 00051 00058 private $tmpFiles = array(); 00059 00066 private $mwGlobals = array(); 00067 00071 const DB_PREFIX = 'unittest_'; 00072 const ORA_DB_PREFIX = 'ut_'; 00073 00078 protected $supportedDBs = array( 00079 'mysql', 00080 'sqlite', 00081 'postgres', 00082 'oracle' 00083 ); 00084 00085 public function __construct( $name = null, array $data = array(), $dataName = '' ) { 00086 parent::__construct( $name, $data, $dataName ); 00087 00088 $this->backupGlobals = false; 00089 $this->backupStaticAttributes = false; 00090 } 00091 00092 public function run( PHPUnit_Framework_TestResult $result = null ) { 00093 /* Some functions require some kind of caching, and will end up using the db, 00094 * which we can't allow, as that would open a new connection for mysql. 00095 * Replace with a HashBag. They would not be going to persist anyway. 00096 */ 00097 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; 00098 00099 $needsResetDB = false; 00100 $logName = get_class( $this ) . '::' . $this->getName( false ); 00101 00102 if ( $this->needsDB() ) { 00103 // set up a DB connection for this test to use 00104 00105 self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); 00106 self::$reuseDB = $this->getCliArg( 'reuse-db' ); 00107 00108 $this->db = wfGetDB( DB_MASTER ); 00109 00110 $this->checkDbIsSupported(); 00111 00112 if ( !self::$dbSetup ) { 00113 wfProfileIn( $logName . ' (clone-db)' ); 00114 00115 // switch to a temporary clone of the database 00116 self::setupTestDB( $this->db, $this->dbPrefix() ); 00117 00118 if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { 00119 $this->resetDB(); 00120 } 00121 00122 wfProfileOut( $logName . ' (clone-db)' ); 00123 } 00124 00125 wfProfileIn( $logName . ' (prepare-db)' ); 00126 $this->addCoreDBData(); 00127 $this->addDBData(); 00128 wfProfileOut( $logName . ' (prepare-db)' ); 00129 00130 $needsResetDB = true; 00131 } 00132 00133 wfProfileIn( $logName ); 00134 parent::run( $result ); 00135 wfProfileOut( $logName ); 00136 00137 if ( $needsResetDB ) { 00138 wfProfileIn( $logName . ' (reset-db)' ); 00139 $this->resetDB(); 00140 wfProfileOut( $logName . ' (reset-db)' ); 00141 } 00142 } 00143 00149 public function usesTemporaryTables() { 00150 return self::$useTemporaryTables; 00151 } 00152 00162 protected function getNewTempFile() { 00163 $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); 00164 $this->tmpFiles[] = $fileName; 00165 00166 return $fileName; 00167 } 00168 00179 protected function getNewTempDirectory() { 00180 // Starting of with a temporary /file/. 00181 $fileName = $this->getNewTempFile(); 00182 00183 // Converting the temporary /file/ to a /directory/ 00184 // 00185 // The following is not atomic, but at least we now have a single place, 00186 // where temporary directory creation is bundled and can be improved 00187 unlink( $fileName ); 00188 $this->assertTrue( wfMkdirParents( $fileName ) ); 00189 00190 return $fileName; 00191 } 00192 00193 protected function setUp() { 00194 wfProfileIn( __METHOD__ ); 00195 parent::setUp(); 00196 $this->called['setUp'] = 1; 00197 00198 $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) ); 00199 00200 // Cleaning up temporary files 00201 foreach ( $this->tmpFiles as $fileName ) { 00202 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) { 00203 unlink( $fileName ); 00204 } elseif ( is_dir( $fileName ) ) { 00205 wfRecursiveRemoveDir( $fileName ); 00206 } 00207 } 00208 00209 if ( $this->needsDB() && $this->db ) { 00210 // Clean up open transactions 00211 while ( $this->db->trxLevel() > 0 ) { 00212 $this->db->rollback(); 00213 } 00214 00215 // don't ignore DB errors 00216 $this->db->ignoreErrors( false ); 00217 } 00218 00219 wfProfileOut( __METHOD__ ); 00220 } 00221 00222 protected function tearDown() { 00223 wfProfileIn( __METHOD__ ); 00224 00225 // Cleaning up temporary files 00226 foreach ( $this->tmpFiles as $fileName ) { 00227 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) { 00228 unlink( $fileName ); 00229 } elseif ( is_dir( $fileName ) ) { 00230 wfRecursiveRemoveDir( $fileName ); 00231 } 00232 } 00233 00234 if ( $this->needsDB() && $this->db ) { 00235 // Clean up open transactions 00236 while ( $this->db->trxLevel() > 0 ) { 00237 $this->db->rollback(); 00238 } 00239 00240 // don't ignore DB errors 00241 $this->db->ignoreErrors( false ); 00242 } 00243 00244 // Restore mw globals 00245 foreach ( $this->mwGlobals as $key => $value ) { 00246 $GLOBALS[$key] = $value; 00247 } 00248 $this->mwGlobals = array(); 00249 00250 $phpErrorLevel = intval( ini_get( 'error_reporting' ) ); 00251 00252 if ( $phpErrorLevel !== $this->phpErrorLevel ) { 00253 ini_set( 'error_reporting', $this->phpErrorLevel ); 00254 00255 $oldHex = strtoupper( dechex( $this->phpErrorLevel ) ); 00256 $newHex = strtoupper( dechex( $phpErrorLevel ) ); 00257 $message = "PHP error_reporting setting was left dirty: was 0x$oldHex before test, 0x$newHex after test!"; 00258 00259 $this->fail( $message ); 00260 } 00261 00262 parent::tearDown(); 00263 wfProfileOut( __METHOD__ ); 00264 } 00265 00270 final public function testMediaWikiTestCaseParentSetupCalled() { 00271 $this->assertArrayHasKey( 'setUp', $this->called, 00272 get_called_class() . "::setUp() must call parent::setUp()" 00273 ); 00274 } 00275 00308 protected function setMwGlobals( $pairs, $value = null ) { 00309 if ( is_string( $pairs ) ) { 00310 $pairs = array( $pairs => $value ); 00311 } 00312 00313 $this->stashMwGlobals( array_keys( $pairs ) ); 00314 00315 foreach ( $pairs as $key => $value ) { 00316 $GLOBALS[$key] = $value; 00317 } 00318 } 00319 00335 protected function stashMwGlobals( $globalKeys ) { 00336 if ( is_string( $globalKeys ) ) { 00337 $globalKeys = array( $globalKeys ); 00338 } 00339 00340 foreach ( $globalKeys as $globalKey ) { 00341 // NOTE: make sure we only save the global once or a second call to 00342 // setMwGlobals() on the same global would override the original 00343 // value. 00344 if ( !array_key_exists( $globalKey, $this->mwGlobals ) ) { 00345 if ( !array_key_exists( $globalKey, $GLOBALS ) ) { 00346 throw new Exception( "Global with key {$globalKey} doesn't exist and cant be stashed" ); 00347 } 00348 // NOTE: we serialize then unserialize the value in case it is an object 00349 // this stops any objects being passed by reference. We could use clone 00350 // and if is_object but this does account for objects within objects! 00351 try { 00352 $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) ); 00353 } 00354 // NOTE; some things such as Closures are not serializable 00355 // in this case just set the value! 00356 catch ( Exception $e ) { 00357 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey]; 00358 } 00359 } 00360 } 00361 } 00362 00375 protected function mergeMwGlobalArrayValue( $name, $values ) { 00376 if ( !isset( $GLOBALS[$name] ) ) { 00377 $merged = $values; 00378 } else { 00379 if ( !is_array( $GLOBALS[$name] ) ) { 00380 throw new MWException( "MW global $name is not an array." ); 00381 } 00382 00383 // NOTE: do not use array_merge, it screws up for numeric keys. 00384 $merged = $GLOBALS[$name]; 00385 foreach ( $values as $k => $v ) { 00386 $merged[$k] = $v; 00387 } 00388 } 00389 00390 $this->setMwGlobals( $name, $merged ); 00391 } 00392 00397 public function dbPrefix() { 00398 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; 00399 } 00400 00405 public function needsDB() { 00406 # if the test says it uses database tables, it needs the database 00407 if ( $this->tablesUsed ) { 00408 return true; 00409 } 00410 00411 # if the test says it belongs to the Database group, it needs the database 00412 $rc = new ReflectionClass( $this ); 00413 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { 00414 return true; 00415 } 00416 00417 return false; 00418 } 00419 00426 public function addDBData() { 00427 } 00428 00429 private function addCoreDBData() { 00430 if ( $this->db->getType() == 'oracle' ) { 00431 00432 # Insert 0 user to prevent FK violations 00433 # Anonymous user 00434 $this->db->insert( 'user', array( 00435 'user_id' => 0, 00436 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); 00437 00438 # Insert 0 page to prevent FK violations 00439 # Blank page 00440 $this->db->insert( 'page', array( 00441 'page_id' => 0, 00442 'page_namespace' => 0, 00443 'page_title' => ' ', 00444 'page_restrictions' => null, 00445 'page_counter' => 0, 00446 'page_is_redirect' => 0, 00447 'page_is_new' => 0, 00448 'page_random' => 0, 00449 'page_touched' => $this->db->timestamp(), 00450 'page_latest' => 0, 00451 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); 00452 } 00453 00454 User::resetIdByNameCache(); 00455 00456 //Make sysop user 00457 $user = User::newFromName( 'UTSysop' ); 00458 00459 if ( $user->idForName() == 0 ) { 00460 $user->addToDatabase(); 00461 $user->setPassword( 'UTSysopPassword' ); 00462 00463 $user->addGroup( 'sysop' ); 00464 $user->addGroup( 'bureaucrat' ); 00465 $user->saveSettings(); 00466 } 00467 00468 //Make 1 page with 1 revision 00469 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); 00470 if ( !$page->getId() == 0 ) { 00471 $page->doEditContent( 00472 new WikitextContent( 'UTContent' ), 00473 'UTPageSummary', 00474 EDIT_NEW, 00475 false, 00476 User::newFromName( 'UTSysop' ) ); 00477 } 00478 } 00479 00487 public static function teardownTestDB() { 00488 if ( !self::$dbSetup ) { 00489 return; 00490 } 00491 00492 CloneDatabase::changePrefix( self::$oldTablePrefix ); 00493 00494 self::$oldTablePrefix = false; 00495 self::$dbSetup = false; 00496 } 00497 00519 public static function setupTestDB( DatabaseBase $db, $prefix ) { 00520 global $wgDBprefix; 00521 if ( $wgDBprefix === $prefix ) { 00522 throw new MWException( 00523 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' ); 00524 } 00525 00526 if ( self::$dbSetup ) { 00527 return; 00528 } 00529 00530 $tablesCloned = self::listTables( $db ); 00531 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); 00532 $dbClone->useTemporaryTables( self::$useTemporaryTables ); 00533 00534 self::$dbSetup = true; 00535 self::$oldTablePrefix = $wgDBprefix; 00536 00537 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { 00538 CloneDatabase::changePrefix( $prefix ); 00539 00540 return; 00541 } else { 00542 $dbClone->cloneTableStructure(); 00543 } 00544 00545 if ( $db->getType() == 'oracle' ) { 00546 $db->query( 'BEGIN FILL_WIKI_INFO; END;' ); 00547 } 00548 } 00549 00553 private function resetDB() { 00554 if ( $this->db ) { 00555 if ( $this->db->getType() == 'oracle' ) { 00556 if ( self::$useTemporaryTables ) { 00557 wfGetLB()->closeAll(); 00558 $this->db = wfGetDB( DB_MASTER ); 00559 } else { 00560 foreach ( $this->tablesUsed as $tbl ) { 00561 if ( $tbl == 'interwiki' ) { 00562 continue; 00563 } 00564 $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ ); 00565 } 00566 } 00567 } else { 00568 foreach ( $this->tablesUsed as $tbl ) { 00569 if ( $tbl == 'interwiki' || $tbl == 'user' ) { 00570 continue; 00571 } 00572 $this->db->delete( $tbl, '*', __METHOD__ ); 00573 } 00574 } 00575 } 00576 } 00577 00587 public function __call( $func, $args ) { 00588 static $compatibility = array( 00589 'assertEmpty' => 'assertEmpty2', // assertEmpty was added in phpunit 3.7.32 00590 ); 00591 00592 if ( isset( $compatibility[$func] ) ) { 00593 return call_user_func_array( array( $this, $compatibility[$func] ), $args ); 00594 } else { 00595 throw new MWException( "Called non-existant $func method on " 00596 . get_class( $this ) ); 00597 } 00598 } 00599 00603 private function assertEmpty2( $value, $msg ) { 00604 return $this->assertTrue( $value == '', $msg ); 00605 } 00606 00607 private static function unprefixTable( $tableName ) { 00608 global $wgDBprefix; 00609 00610 return substr( $tableName, strlen( $wgDBprefix ) ); 00611 } 00612 00613 private static function isNotUnittest( $table ) { 00614 return strpos( $table, 'unittest_' ) !== 0; 00615 } 00616 00624 public static function listTables( $db ) { 00625 global $wgDBprefix; 00626 00627 $tables = $db->listTables( $wgDBprefix, __METHOD__ ); 00628 00629 if ( $db->getType() === 'mysql' ) { 00630 # bug 43571: cannot clone VIEWs under MySQL 00631 $views = $db->listViews( $wgDBprefix, __METHOD__ ); 00632 $tables = array_diff( $tables, $views ); 00633 } 00634 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); 00635 00636 // Don't duplicate test tables from the previous fataled run 00637 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); 00638 00639 if ( $db->getType() == 'sqlite' ) { 00640 $tables = array_flip( $tables ); 00641 // these are subtables of searchindex and don't need to be duped/dropped separately 00642 unset( $tables['searchindex_content'] ); 00643 unset( $tables['searchindex_segdir'] ); 00644 unset( $tables['searchindex_segments'] ); 00645 $tables = array_flip( $tables ); 00646 } 00647 00648 return $tables; 00649 } 00650 00655 protected function checkDbIsSupported() { 00656 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) { 00657 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); 00658 } 00659 } 00660 00664 public function getCliArg( $offset ) { 00665 if ( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { 00666 return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; 00667 } 00668 } 00669 00673 public function setCliArg( $offset, $value ) { 00674 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; 00675 } 00676 00685 public function hideDeprecated( $function ) { 00686 wfSuppressWarnings(); 00687 wfDeprecated( $function ); 00688 wfRestoreWarnings(); 00689 } 00690 00709 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) { 00710 if ( !$this->needsDB() ) { 00711 throw new MWException( 'When testing database state, the test cases\'s needDB()' . 00712 ' method should return true. Use @group Database or $this->tablesUsed.' ); 00713 } 00714 00715 $db = wfGetDB( DB_SLAVE ); 00716 00717 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); 00718 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); 00719 00720 $i = 0; 00721 00722 foreach ( $expectedRows as $expected ) { 00723 $r = $res->fetchRow(); 00724 self::stripStringKeys( $r ); 00725 00726 $i += 1; 00727 $this->assertNotEmpty( $r, "row #$i missing" ); 00728 00729 $this->assertEquals( $expected, $r, "row #$i mismatches" ); 00730 } 00731 00732 $r = $res->fetchRow(); 00733 self::stripStringKeys( $r ); 00734 00735 $this->assertFalse( $r, "found extra row (after #$i)" ); 00736 } 00737 00749 protected function arrayWrap( array $elements ) { 00750 return array_map( 00751 function ( $element ) { 00752 return array( $element ); 00753 }, 00754 $elements 00755 ); 00756 } 00757 00770 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { 00771 if ( !$ordered ) { 00772 $this->objectAssociativeSort( $expected ); 00773 $this->objectAssociativeSort( $actual ); 00774 } 00775 00776 if ( !$named ) { 00777 $expected = array_values( $expected ); 00778 $actual = array_values( $actual ); 00779 } 00780 00781 call_user_func_array( 00782 array( $this, 'assertEquals' ), 00783 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) 00784 ); 00785 } 00786 00799 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) { 00800 $expected = str_replace( '>', ">\n", $expected ); 00801 $actual = str_replace( '>', ">\n", $actual ); 00802 00803 $this->assertEquals( $expected, $actual, $msg ); 00804 } 00805 00813 protected function objectAssociativeSort( array &$array ) { 00814 uasort( 00815 $array, 00816 function ( $a, $b ) { 00817 return serialize( $a ) > serialize( $b ) ? 1 : -1; 00818 } 00819 ); 00820 } 00821 00831 protected static function stripStringKeys( &$r ) { 00832 if ( !is_array( $r ) ) { 00833 return; 00834 } 00835 00836 foreach ( $r as $k => $v ) { 00837 if ( is_string( $k ) ) { 00838 unset( $r[$k] ); 00839 } 00840 } 00841 } 00842 00856 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { 00857 if ( $actual === $value ) { 00858 $this->assertTrue( true, $message ); 00859 } else { 00860 $this->assertType( $type, $actual, $message ); 00861 } 00862 } 00863 00875 protected function assertType( $type, $actual, $message = '' ) { 00876 if ( class_exists( $type ) || interface_exists( $type ) ) { 00877 $this->assertInstanceOf( $type, $actual, $message ); 00878 } else { 00879 $this->assertInternalType( $type, $actual, $message ); 00880 } 00881 } 00882 00892 protected function isWikitextNS( $ns ) { 00893 global $wgNamespaceContentModels; 00894 00895 if ( isset( $wgNamespaceContentModels[$ns] ) ) { 00896 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT; 00897 } 00898 00899 return true; 00900 } 00901 00909 protected function getDefaultWikitextNS() { 00910 global $wgNamespaceContentModels; 00911 00912 static $wikitextNS = null; // this is not going to change 00913 if ( $wikitextNS !== null ) { 00914 return $wikitextNS; 00915 } 00916 00917 // quickly short out on most common case: 00918 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) { 00919 return NS_MAIN; 00920 } 00921 00922 // NOTE: prefer content namespaces 00923 $namespaces = array_unique( array_merge( 00924 MWNamespace::getContentNamespaces(), 00925 array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these 00926 MWNamespace::getValidNamespaces() 00927 ) ); 00928 00929 $namespaces = array_diff( $namespaces, array( 00930 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces 00931 ) ); 00932 00933 $talk = array_filter( $namespaces, function ( $ns ) { 00934 return MWNamespace::isTalk( $ns ); 00935 } ); 00936 00937 // prefer non-talk pages 00938 $namespaces = array_diff( $namespaces, $talk ); 00939 $namespaces = array_merge( $namespaces, $talk ); 00940 00941 // check default content model of each namespace 00942 foreach ( $namespaces as $ns ) { 00943 if ( !isset( $wgNamespaceContentModels[$ns] ) || 00944 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT 00945 ) { 00946 00947 $wikitextNS = $ns; 00948 00949 return $wikitextNS; 00950 } 00951 } 00952 00953 // give up 00954 // @todo Inside a test, we could skip the test as incomplete. 00955 // But frequently, this is used in fixture setup. 00956 throw new MWException( "No namespace defaults to wikitext!" ); 00957 } 00958 00965 protected function checkHasDiff3() { 00966 global $wgDiff3; 00967 00968 # This check may also protect against code injection in 00969 # case of broken installations. 00970 wfSuppressWarnings(); 00971 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); 00972 wfRestoreWarnings(); 00973 00974 if ( !$haveDiff3 ) { 00975 $this->markTestSkipped( "Skip test, since diff3 is not configured" ); 00976 } 00977 } 00978 00989 protected function checkHasGzip() { 00990 static $haveGzip; 00991 00992 if ( $haveGzip === null ) { 00993 $retval = null; 00994 wfShellExec( 'gzip -V', $retval ); 00995 $haveGzip = ( $retval === 0 ); 00996 } 00997 00998 if ( !$haveGzip ) { 00999 $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); 01000 } 01001 01002 return $haveGzip; 01003 } 01004 01011 protected function checkPHPExtension( $extName ) { 01012 $loaded = extension_loaded( $extName ); 01013 if ( !$loaded ) { 01014 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." ); 01015 } 01016 01017 return $loaded; 01018 } 01019 01031 protected function assertException( $code, $expected = 'Exception', $message = '' ) { 01032 $pokemons = null; 01033 01034 try { 01035 call_user_func( $code ); 01036 } catch ( Exception $pokemons ) { 01037 // Gotta Catch 'Em All! 01038 } 01039 01040 if ( $message === '' ) { 01041 $message = 'An exception of type "' . $expected . '" should have been thrown'; 01042 } 01043 01044 $this->assertInstanceOf( $expected, $pokemons, $message ); 01045 } 01046 01061 protected function assertValidHtmlSnippet( $html ) { 01062 $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>'; 01063 $this->assertValidHtmlDocument( $html ); 01064 } 01065 01077 protected function assertValidHtmlDocument( $html ) { 01078 // Note: we only validate if the tidy PHP extension is available. 01079 // In case wgTidyInternal is false, MWTidy would fall back to the command line version 01080 // of tidy. In that case however, we can not reliably detect whether a failing validation 01081 // is due to malformed HTML, or caused by tidy not being installed as a command line tool. 01082 // That would cause all HTML assertions to fail on a system that has no tidy installed. 01083 if ( !$GLOBALS['wgTidyInternal'] ) { 01084 $this->markTestSkipped( 'Tidy extension not installed' ); 01085 } 01086 01087 $errorBuffer = ''; 01088 MWTidy::checkErrors( $html, $errorBuffer ); 01089 $allErrors = preg_split( '/[\r\n]+/', $errorBuffer ); 01090 01091 // Filter Tidy warnings which aren't useful for us. 01092 // Tidy eg. often cries about parameters missing which have actually 01093 // been deprecated since HTML4, thus we should not care about them. 01094 $errors = preg_grep( 01095 '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m', 01096 $allErrors, PREG_GREP_INVERT 01097 ); 01098 01099 $this->assertEmpty( $errors, implode( "\n", $errors ) ); 01100 } 01101 01102 }