MediaWikiTestCase.php 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. <?php
  2. /**
  3. * @since 1.18
  4. */
  5. abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
  6. /**
  7. * $called tracks whether the setUp and tearDown method has been called.
  8. * class extending MediaWikiTestCase usually override setUp and tearDown
  9. * but forget to call the parent.
  10. *
  11. * The array format takes a method name as key and anything as a value.
  12. * By asserting the key exist, we know the child class has called the
  13. * parent.
  14. *
  15. * This property must be private, we do not want child to override it,
  16. * they should call the appropriate parent method instead.
  17. */
  18. private $called = array();
  19. /**
  20. * @var TestUser[]
  21. * @since 1.20
  22. */
  23. public static $users;
  24. /**
  25. * @var DatabaseBase
  26. * @since 1.18
  27. */
  28. protected $db;
  29. /**
  30. * @var array
  31. * @since 1.19
  32. */
  33. protected $tablesUsed = array(); // tables with data
  34. private static $useTemporaryTables = true;
  35. private static $reuseDB = false;
  36. private static $dbSetup = false;
  37. private static $oldTablePrefix = false;
  38. /**
  39. * Original value of PHP's error_reporting setting.
  40. *
  41. * @var int
  42. */
  43. private $phpErrorLevel;
  44. /**
  45. * Holds the paths of temporary files/directories created through getNewTempFile,
  46. * and getNewTempDirectory
  47. *
  48. * @var array
  49. */
  50. private $tmpFiles = array();
  51. /**
  52. * Holds original values of MediaWiki configuration settings
  53. * to be restored in tearDown().
  54. * See also setMwGlobals().
  55. * @var array
  56. */
  57. private $mwGlobals = array();
  58. /**
  59. * Table name prefixes. Oracle likes it shorter.
  60. */
  61. const DB_PREFIX = 'unittest_';
  62. const ORA_DB_PREFIX = 'ut_';
  63. /**
  64. * @var array
  65. * @since 1.18
  66. */
  67. protected $supportedDBs = array(
  68. 'mysql',
  69. 'sqlite',
  70. 'postgres',
  71. 'oracle'
  72. );
  73. public function __construct( $name = null, array $data = array(), $dataName = '' ) {
  74. parent::__construct( $name, $data, $dataName );
  75. $this->backupGlobals = false;
  76. $this->backupStaticAttributes = false;
  77. }
  78. public function run( PHPUnit_Framework_TestResult $result = null ) {
  79. /* Some functions require some kind of caching, and will end up using the db,
  80. * which we can't allow, as that would open a new connection for mysql.
  81. * Replace with a HashBag. They would not be going to persist anyway.
  82. */
  83. ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
  84. $needsResetDB = false;
  85. $logName = get_class( $this ) . '::' . $this->getName( false );
  86. if ( $this->needsDB() ) {
  87. // set up a DB connection for this test to use
  88. self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
  89. self::$reuseDB = $this->getCliArg( 'reuse-db' );
  90. $this->db = wfGetDB( DB_MASTER );
  91. $this->checkDbIsSupported();
  92. if ( !self::$dbSetup ) {
  93. wfProfileIn( $logName . ' (clone-db)' );
  94. // switch to a temporary clone of the database
  95. self::setupTestDB( $this->db, $this->dbPrefix() );
  96. if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
  97. $this->resetDB();
  98. }
  99. wfProfileOut( $logName . ' (clone-db)' );
  100. }
  101. wfProfileIn( $logName . ' (prepare-db)' );
  102. $this->addCoreDBData();
  103. $this->addDBData();
  104. wfProfileOut( $logName . ' (prepare-db)' );
  105. $needsResetDB = true;
  106. }
  107. wfProfileIn( $logName );
  108. parent::run( $result );
  109. wfProfileOut( $logName );
  110. if ( $needsResetDB ) {
  111. wfProfileIn( $logName . ' (reset-db)' );
  112. $this->resetDB();
  113. wfProfileOut( $logName . ' (reset-db)' );
  114. }
  115. }
  116. /**
  117. * @since 1.21
  118. *
  119. * @return bool
  120. */
  121. public function usesTemporaryTables() {
  122. return self::$useTemporaryTables;
  123. }
  124. /**
  125. * Obtains a new temporary file name
  126. *
  127. * The obtained filename is enlisted to be removed upon tearDown
  128. *
  129. * @since 1.20
  130. *
  131. * @return string absolute name of the temporary file
  132. */
  133. protected function getNewTempFile() {
  134. $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
  135. $this->tmpFiles[] = $fileName;
  136. return $fileName;
  137. }
  138. /**
  139. * obtains a new temporary directory
  140. *
  141. * The obtained directory is enlisted to be removed (recursively with all its contained
  142. * files) upon tearDown.
  143. *
  144. * @since 1.20
  145. *
  146. * @return string Absolute name of the temporary directory
  147. */
  148. protected function getNewTempDirectory() {
  149. // Starting of with a temporary /file/.
  150. $fileName = $this->getNewTempFile();
  151. // Converting the temporary /file/ to a /directory/
  152. //
  153. // The following is not atomic, but at least we now have a single place,
  154. // where temporary directory creation is bundled and can be improved
  155. unlink( $fileName );
  156. $this->assertTrue( wfMkdirParents( $fileName ) );
  157. return $fileName;
  158. }
  159. protected function setUp() {
  160. wfProfileIn( __METHOD__ );
  161. parent::setUp();
  162. $this->called['setUp'] = 1;
  163. $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
  164. // Cleaning up temporary files
  165. foreach ( $this->tmpFiles as $fileName ) {
  166. if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
  167. unlink( $fileName );
  168. } elseif ( is_dir( $fileName ) ) {
  169. wfRecursiveRemoveDir( $fileName );
  170. }
  171. }
  172. if ( $this->needsDB() && $this->db ) {
  173. // Clean up open transactions
  174. while ( $this->db->trxLevel() > 0 ) {
  175. $this->db->rollback();
  176. }
  177. // don't ignore DB errors
  178. $this->db->ignoreErrors( false );
  179. }
  180. wfProfileOut( __METHOD__ );
  181. }
  182. protected function tearDown() {
  183. wfProfileIn( __METHOD__ );
  184. // Cleaning up temporary files
  185. foreach ( $this->tmpFiles as $fileName ) {
  186. if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
  187. unlink( $fileName );
  188. } elseif ( is_dir( $fileName ) ) {
  189. wfRecursiveRemoveDir( $fileName );
  190. }
  191. }
  192. if ( $this->needsDB() && $this->db ) {
  193. // Clean up open transactions
  194. while ( $this->db->trxLevel() > 0 ) {
  195. $this->db->rollback();
  196. }
  197. // don't ignore DB errors
  198. $this->db->ignoreErrors( false );
  199. }
  200. // Restore mw globals
  201. foreach ( $this->mwGlobals as $key => $value ) {
  202. $GLOBALS[$key] = $value;
  203. }
  204. $this->mwGlobals = array();
  205. $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
  206. if ( $phpErrorLevel !== $this->phpErrorLevel ) {
  207. ini_set( 'error_reporting', $this->phpErrorLevel );
  208. $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
  209. $newHex = strtoupper( dechex( $phpErrorLevel ) );
  210. $message = "PHP error_reporting setting was left dirty: was 0x$oldHex before test, 0x$newHex after test!";
  211. $this->fail( $message );
  212. }
  213. parent::tearDown();
  214. wfProfileOut( __METHOD__ );
  215. }
  216. /**
  217. * Make sure MediaWikiTestCase extending classes have called their
  218. * parent setUp method
  219. */
  220. final public function testMediaWikiTestCaseParentSetupCalled() {
  221. $this->assertArrayHasKey( 'setUp', $this->called,
  222. get_called_class() . "::setUp() must call parent::setUp()"
  223. );
  224. }
  225. /**
  226. * Sets a global, maintaining a stashed version of the previous global to be
  227. * restored in tearDown
  228. *
  229. * The key is added to the array of globals that will be reset afterwards
  230. * in the tearDown().
  231. *
  232. * @example
  233. * <code>
  234. * protected function setUp() {
  235. * $this->setMwGlobals( 'wgRestrictStuff', true );
  236. * }
  237. *
  238. * function testFoo() {}
  239. *
  240. * function testBar() {}
  241. * $this->assertTrue( self::getX()->doStuff() );
  242. *
  243. * $this->setMwGlobals( 'wgRestrictStuff', false );
  244. * $this->assertTrue( self::getX()->doStuff() );
  245. * }
  246. *
  247. * function testQuux() {}
  248. * </code>
  249. *
  250. * @param array|string $pairs Key to the global variable, or an array
  251. * of key/value pairs.
  252. * @param mixed $value Value to set the global to (ignored
  253. * if an array is given as first argument).
  254. *
  255. * @since 1.21
  256. */
  257. protected function setMwGlobals( $pairs, $value = null ) {
  258. if ( is_string( $pairs ) ) {
  259. $pairs = array( $pairs => $value );
  260. }
  261. $this->stashMwGlobals( array_keys( $pairs ) );
  262. foreach ( $pairs as $key => $value ) {
  263. $GLOBALS[$key] = $value;
  264. }
  265. }
  266. /**
  267. * Stashes the global, will be restored in tearDown()
  268. *
  269. * Individual test functions may override globals through the setMwGlobals() function
  270. * or directly. When directly overriding globals their keys should first be passed to this
  271. * method in setUp to avoid breaking global state for other tests
  272. *
  273. * That way all other tests are executed with the same settings (instead of using the
  274. * unreliable local settings for most tests and fix it only for some tests).
  275. *
  276. * @param array|string $globalKeys Key to the global variable, or an array of keys.
  277. *
  278. * @throws Exception when trying to stash an unset global
  279. * @since 1.23
  280. */
  281. protected function stashMwGlobals( $globalKeys ) {
  282. if ( is_string( $globalKeys ) ) {
  283. $globalKeys = array( $globalKeys );
  284. }
  285. foreach ( $globalKeys as $globalKey ) {
  286. // NOTE: make sure we only save the global once or a second call to
  287. // setMwGlobals() on the same global would override the original
  288. // value.
  289. if ( !array_key_exists( $globalKey, $this->mwGlobals ) ) {
  290. if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
  291. throw new Exception( "Global with key {$globalKey} doesn't exist and cant be stashed" );
  292. }
  293. // NOTE: we serialize then unserialize the value in case it is an object
  294. // this stops any objects being passed by reference. We could use clone
  295. // and if is_object but this does account for objects within objects!
  296. try {
  297. $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
  298. }
  299. // NOTE; some things such as Closures are not serializable
  300. // in this case just set the value!
  301. catch ( Exception $e ) {
  302. $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
  303. }
  304. }
  305. }
  306. }
  307. /**
  308. * Merges the given values into a MW global array variable.
  309. * Useful for setting some entries in a configuration array, instead of
  310. * setting the entire array.
  311. *
  312. * @param string $name The name of the global, as in wgFooBar
  313. * @param array $values The array containing the entries to set in that global
  314. *
  315. * @throws MWException if the designated global is not an array.
  316. *
  317. * @since 1.21
  318. */
  319. protected function mergeMwGlobalArrayValue( $name, $values ) {
  320. if ( !isset( $GLOBALS[$name] ) ) {
  321. $merged = $values;
  322. } else {
  323. if ( !is_array( $GLOBALS[$name] ) ) {
  324. throw new MWException( "MW global $name is not an array." );
  325. }
  326. // NOTE: do not use array_merge, it screws up for numeric keys.
  327. $merged = $GLOBALS[$name];
  328. foreach ( $values as $k => $v ) {
  329. $merged[$k] = $v;
  330. }
  331. }
  332. $this->setMwGlobals( $name, $merged );
  333. }
  334. /**
  335. * @return string
  336. * @since 1.18
  337. */
  338. public function dbPrefix() {
  339. return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
  340. }
  341. /**
  342. * @return bool
  343. * @since 1.18
  344. */
  345. public function needsDB() {
  346. # if the test says it uses database tables, it needs the database
  347. if ( $this->tablesUsed ) {
  348. return true;
  349. }
  350. # if the test says it belongs to the Database group, it needs the database
  351. $rc = new ReflectionClass( $this );
  352. if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
  353. return true;
  354. }
  355. return false;
  356. }
  357. /**
  358. * Stub. If a test needs to add additional data to the database, it should
  359. * implement this method and do so
  360. *
  361. * @since 1.18
  362. */
  363. public function addDBData() {
  364. }
  365. private function addCoreDBData() {
  366. if ( $this->db->getType() == 'oracle' ) {
  367. # Insert 0 user to prevent FK violations
  368. # Anonymous user
  369. $this->db->insert( 'user', array(
  370. 'user_id' => 0,
  371. 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) );
  372. # Insert 0 page to prevent FK violations
  373. # Blank page
  374. $this->db->insert( 'page', array(
  375. 'page_id' => 0,
  376. 'page_namespace' => 0,
  377. 'page_title' => ' ',
  378. 'page_restrictions' => null,
  379. 'page_counter' => 0,
  380. 'page_is_redirect' => 0,
  381. 'page_is_new' => 0,
  382. 'page_random' => 0,
  383. 'page_touched' => $this->db->timestamp(),
  384. 'page_latest' => 0,
  385. 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) );
  386. }
  387. User::resetIdByNameCache();
  388. //Make sysop user
  389. $user = User::newFromName( 'UTSysop' );
  390. if ( $user->idForName() == 0 ) {
  391. $user->addToDatabase();
  392. $user->setPassword( 'UTSysopPassword' );
  393. $user->addGroup( 'sysop' );
  394. $user->addGroup( 'bureaucrat' );
  395. $user->saveSettings();
  396. }
  397. //Make 1 page with 1 revision
  398. $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
  399. if ( !$page->getId() == 0 ) {
  400. $page->doEditContent(
  401. new WikitextContent( 'UTContent' ),
  402. 'UTPageSummary',
  403. EDIT_NEW,
  404. false,
  405. User::newFromName( 'UTSysop' ) );
  406. }
  407. }
  408. /**
  409. * Restores MediaWiki to using the table set (table prefix) it was using before
  410. * setupTestDB() was called. Useful if we need to perform database operations
  411. * after the test run has finished (such as saving logs or profiling info).
  412. *
  413. * @since 1.21
  414. */
  415. public static function teardownTestDB() {
  416. if ( !self::$dbSetup ) {
  417. return;
  418. }
  419. CloneDatabase::changePrefix( self::$oldTablePrefix );
  420. self::$oldTablePrefix = false;
  421. self::$dbSetup = false;
  422. }
  423. /**
  424. * Creates an empty skeleton of the wiki database by cloning its structure
  425. * to equivalent tables using the given $prefix. Then sets MediaWiki to
  426. * use the new set of tables (aka schema) instead of the original set.
  427. *
  428. * This is used to generate a dummy table set, typically consisting of temporary
  429. * tables, that will be used by tests instead of the original wiki database tables.
  430. *
  431. * @since 1.21
  432. *
  433. * @note: the original table prefix is stored in self::$oldTablePrefix. This is used
  434. * by teardownTestDB() to return the wiki to using the original table set.
  435. *
  436. * @note: this method only works when first called. Subsequent calls have no effect,
  437. * even if using different parameters.
  438. *
  439. * @param DatabaseBase $db The database connection
  440. * @param String $prefix The prefix to use for the new table set (aka schema).
  441. *
  442. * @throws MWException if the database table prefix is already $prefix
  443. */
  444. public static function setupTestDB( DatabaseBase $db, $prefix ) {
  445. global $wgDBprefix;
  446. if ( $wgDBprefix === $prefix ) {
  447. throw new MWException(
  448. 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
  449. }
  450. if ( self::$dbSetup ) {
  451. return;
  452. }
  453. $tablesCloned = self::listTables( $db );
  454. $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
  455. $dbClone->useTemporaryTables( self::$useTemporaryTables );
  456. self::$dbSetup = true;
  457. self::$oldTablePrefix = $wgDBprefix;
  458. if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
  459. CloneDatabase::changePrefix( $prefix );
  460. return;
  461. } else {
  462. $dbClone->cloneTableStructure();
  463. }
  464. if ( $db->getType() == 'oracle' ) {
  465. $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
  466. }
  467. }
  468. /**
  469. * Empty all tables so they can be repopulated for tests
  470. */
  471. private function resetDB() {
  472. if ( $this->db ) {
  473. if ( $this->db->getType() == 'oracle' ) {
  474. if ( self::$useTemporaryTables ) {
  475. wfGetLB()->closeAll();
  476. $this->db = wfGetDB( DB_MASTER );
  477. } else {
  478. foreach ( $this->tablesUsed as $tbl ) {
  479. if ( $tbl == 'interwiki' ) {
  480. continue;
  481. }
  482. $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
  483. }
  484. }
  485. } else {
  486. foreach ( $this->tablesUsed as $tbl ) {
  487. if ( $tbl == 'interwiki' || $tbl == 'user' ) {
  488. continue;
  489. }
  490. $this->db->delete( $tbl, '*', __METHOD__ );
  491. }
  492. }
  493. }
  494. }
  495. /**
  496. * @since 1.18
  497. *
  498. * @param string $func
  499. * @param array $args
  500. *
  501. * @return mixed
  502. * @throws MWException
  503. */
  504. public function __call( $func, $args ) {
  505. static $compatibility = array(
  506. 'assertEmpty' => 'assertEmpty2', // assertEmpty was added in phpunit 3.7.32
  507. );
  508. if ( isset( $compatibility[$func] ) ) {
  509. return call_user_func_array( array( $this, $compatibility[$func] ), $args );
  510. } else {
  511. throw new MWException( "Called non-existant $func method on "
  512. . get_class( $this ) );
  513. }
  514. }
  515. /**
  516. * Used as a compatibility method for phpunit < 3.7.32
  517. */
  518. private function assertEmpty2( $value, $msg ) {
  519. return $this->assertTrue( $value == '', $msg );
  520. }
  521. private static function unprefixTable( $tableName ) {
  522. global $wgDBprefix;
  523. return substr( $tableName, strlen( $wgDBprefix ) );
  524. }
  525. private static function isNotUnittest( $table ) {
  526. return strpos( $table, 'unittest_' ) !== 0;
  527. }
  528. /**
  529. * @since 1.18
  530. *
  531. * @param DataBaseBase $db
  532. *
  533. * @return array
  534. */
  535. public static function listTables( $db ) {
  536. global $wgDBprefix;
  537. $tables = $db->listTables( $wgDBprefix, __METHOD__ );
  538. if ( $db->getType() === 'mysql' ) {
  539. # bug 43571: cannot clone VIEWs under MySQL
  540. $views = $db->listViews( $wgDBprefix, __METHOD__ );
  541. $tables = array_diff( $tables, $views );
  542. }
  543. $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables );
  544. // Don't duplicate test tables from the previous fataled run
  545. $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) );
  546. if ( $db->getType() == 'sqlite' ) {
  547. $tables = array_flip( $tables );
  548. // these are subtables of searchindex and don't need to be duped/dropped separately
  549. unset( $tables['searchindex_content'] );
  550. unset( $tables['searchindex_segdir'] );
  551. unset( $tables['searchindex_segments'] );
  552. $tables = array_flip( $tables );
  553. }
  554. return $tables;
  555. }
  556. /**
  557. * @throws MWException
  558. * @since 1.18
  559. */
  560. protected function checkDbIsSupported() {
  561. if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
  562. throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
  563. }
  564. }
  565. /**
  566. * @since 1.18
  567. */
  568. public function getCliArg( $offset ) {
  569. if ( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) {
  570. return MediaWikiPHPUnitCommand::$additionalOptions[$offset];
  571. }
  572. }
  573. /**
  574. * @since 1.18
  575. */
  576. public function setCliArg( $offset, $value ) {
  577. MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value;
  578. }
  579. /**
  580. * Don't throw a warning if $function is deprecated and called later
  581. *
  582. * @since 1.19
  583. *
  584. * @param string $function
  585. * @return null
  586. */
  587. public function hideDeprecated( $function ) {
  588. wfSuppressWarnings();
  589. wfDeprecated( $function );
  590. wfRestoreWarnings();
  591. }
  592. /**
  593. * Asserts that the given database query yields the rows given by $expectedRows.
  594. * The expected rows should be given as indexed (not associative) arrays, with
  595. * the values given in the order of the columns in the $fields parameter.
  596. * Note that the rows are sorted by the columns given in $fields.
  597. *
  598. * @since 1.20
  599. *
  600. * @param string|array $table The table(s) to query
  601. * @param string|array $fields The columns to include in the result (and to sort by)
  602. * @param string|array $condition "where" condition(s)
  603. * @param array $expectedRows An array of arrays giving the expected rows.
  604. *
  605. * @throws MWException If this test cases's needsDB() method doesn't return true.
  606. * Test cases can use "@group Database" to enable database test support,
  607. * or list the tables under testing in $this->tablesUsed, or override the
  608. * needsDB() method.
  609. */
  610. protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
  611. if ( !$this->needsDB() ) {
  612. throw new MWException( 'When testing database state, the test cases\'s needDB()' .
  613. ' method should return true. Use @group Database or $this->tablesUsed.' );
  614. }
  615. $db = wfGetDB( DB_SLAVE );
  616. $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) );
  617. $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
  618. $i = 0;
  619. foreach ( $expectedRows as $expected ) {
  620. $r = $res->fetchRow();
  621. self::stripStringKeys( $r );
  622. $i += 1;
  623. $this->assertNotEmpty( $r, "row #$i missing" );
  624. $this->assertEquals( $expected, $r, "row #$i mismatches" );
  625. }
  626. $r = $res->fetchRow();
  627. self::stripStringKeys( $r );
  628. $this->assertFalse( $r, "found extra row (after #$i)" );
  629. }
  630. /**
  631. * Utility method taking an array of elements and wrapping
  632. * each element in it's own array. Useful for data providers
  633. * that only return a single argument.
  634. *
  635. * @since 1.20
  636. *
  637. * @param array $elements
  638. *
  639. * @return array
  640. */
  641. protected function arrayWrap( array $elements ) {
  642. return array_map(
  643. function ( $element ) {
  644. return array( $element );
  645. },
  646. $elements
  647. );
  648. }
  649. /**
  650. * Assert that two arrays are equal. By default this means that both arrays need to hold
  651. * the same set of values. Using additional arguments, order and associated key can also
  652. * be set as relevant.
  653. *
  654. * @since 1.20
  655. *
  656. * @param array $expected
  657. * @param array $actual
  658. * @param bool $ordered If the order of the values should match
  659. * @param bool $named If the keys should match
  660. */
  661. protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) {
  662. if ( !$ordered ) {
  663. $this->objectAssociativeSort( $expected );
  664. $this->objectAssociativeSort( $actual );
  665. }
  666. if ( !$named ) {
  667. $expected = array_values( $expected );
  668. $actual = array_values( $actual );
  669. }
  670. call_user_func_array(
  671. array( $this, 'assertEquals' ),
  672. array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) )
  673. );
  674. }
  675. /**
  676. * Put each HTML element on its own line and then equals() the results
  677. *
  678. * Use for nicely formatting of PHPUnit diff output when comparing very
  679. * simple HTML
  680. *
  681. * @since 1.20
  682. *
  683. * @param string $expected HTML on oneline
  684. * @param string $actual HTML on oneline
  685. * @param string $msg Optional message
  686. */
  687. protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
  688. $expected = str_replace( '>', ">\n", $expected );
  689. $actual = str_replace( '>', ">\n", $actual );
  690. $this->assertEquals( $expected, $actual, $msg );
  691. }
  692. /**
  693. * Does an associative sort that works for objects.
  694. *
  695. * @since 1.20
  696. *
  697. * @param array $array
  698. */
  699. protected function objectAssociativeSort( array &$array ) {
  700. uasort(
  701. $array,
  702. function ( $a, $b ) {
  703. return serialize( $a ) > serialize( $b ) ? 1 : -1;
  704. }
  705. );
  706. }
  707. /**
  708. * Utility function for eliminating all string keys from an array.
  709. * Useful to turn a database result row as returned by fetchRow() into
  710. * a pure indexed array.
  711. *
  712. * @since 1.20
  713. *
  714. * @param mixed $r The array to remove string keys from.
  715. */
  716. protected static function stripStringKeys( &$r ) {
  717. if ( !is_array( $r ) ) {
  718. return;
  719. }
  720. foreach ( $r as $k => $v ) {
  721. if ( is_string( $k ) ) {
  722. unset( $r[$k] );
  723. }
  724. }
  725. }
  726. /**
  727. * Asserts that the provided variable is of the specified
  728. * internal type or equals the $value argument. This is useful
  729. * for testing return types of functions that return a certain
  730. * type or *value* when not set or on error.
  731. *
  732. * @since 1.20
  733. *
  734. * @param string $type
  735. * @param mixed $actual
  736. * @param mixed $value
  737. * @param string $message
  738. */
  739. protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
  740. if ( $actual === $value ) {
  741. $this->assertTrue( true, $message );
  742. } else {
  743. $this->assertType( $type, $actual, $message );
  744. }
  745. }
  746. /**
  747. * Asserts the type of the provided value. This can be either
  748. * in internal type such as boolean or integer, or a class or
  749. * interface the value extends or implements.
  750. *
  751. * @since 1.20
  752. *
  753. * @param string $type
  754. * @param mixed $actual
  755. * @param string $message
  756. */
  757. protected function assertType( $type, $actual, $message = '' ) {
  758. if ( class_exists( $type ) || interface_exists( $type ) ) {
  759. $this->assertInstanceOf( $type, $actual, $message );
  760. } else {
  761. $this->assertInternalType( $type, $actual, $message );
  762. }
  763. }
  764. /**
  765. * Returns true if the given namespace defaults to Wikitext
  766. * according to $wgNamespaceContentModels
  767. *
  768. * @param int $ns The namespace ID to check
  769. *
  770. * @return bool
  771. * @since 1.21
  772. */
  773. protected function isWikitextNS( $ns ) {
  774. global $wgNamespaceContentModels;
  775. if ( isset( $wgNamespaceContentModels[$ns] ) ) {
  776. return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
  777. }
  778. return true;
  779. }
  780. /**
  781. * Returns the ID of a namespace that defaults to Wikitext.
  782. *
  783. * @throws MWException If there is none.
  784. * @return int The ID of the wikitext Namespace
  785. * @since 1.21
  786. */
  787. protected function getDefaultWikitextNS() {
  788. global $wgNamespaceContentModels;
  789. static $wikitextNS = null; // this is not going to change
  790. if ( $wikitextNS !== null ) {
  791. return $wikitextNS;
  792. }
  793. // quickly short out on most common case:
  794. if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
  795. return NS_MAIN;
  796. }
  797. // NOTE: prefer content namespaces
  798. $namespaces = array_unique( array_merge(
  799. MWNamespace::getContentNamespaces(),
  800. array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these
  801. MWNamespace::getValidNamespaces()
  802. ) );
  803. $namespaces = array_diff( $namespaces, array(
  804. NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
  805. ) );
  806. $talk = array_filter( $namespaces, function ( $ns ) {
  807. return MWNamespace::isTalk( $ns );
  808. } );
  809. // prefer non-talk pages
  810. $namespaces = array_diff( $namespaces, $talk );
  811. $namespaces = array_merge( $namespaces, $talk );
  812. // check default content model of each namespace
  813. foreach ( $namespaces as $ns ) {
  814. if ( !isset( $wgNamespaceContentModels[$ns] ) ||
  815. $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
  816. ) {
  817. $wikitextNS = $ns;
  818. return $wikitextNS;
  819. }
  820. }
  821. // give up
  822. // @todo Inside a test, we could skip the test as incomplete.
  823. // But frequently, this is used in fixture setup.
  824. throw new MWException( "No namespace defaults to wikitext!" );
  825. }
  826. /**
  827. * Check, if $wgDiff3 is set and ready to merge
  828. * Will mark the calling test as skipped, if not ready
  829. *
  830. * @since 1.21
  831. */
  832. protected function checkHasDiff3() {
  833. global $wgDiff3;
  834. # This check may also protect against code injection in
  835. # case of broken installations.
  836. wfSuppressWarnings();
  837. $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
  838. wfRestoreWarnings();
  839. if ( !$haveDiff3 ) {
  840. $this->markTestSkipped( "Skip test, since diff3 is not configured" );
  841. }
  842. }
  843. /**
  844. * Check whether we have the 'gzip' commandline utility, will skip
  845. * the test whenever "gzip -V" fails.
  846. *
  847. * Result is cached at the process level.
  848. *
  849. * @return bool
  850. *
  851. * @since 1.21
  852. */
  853. protected function checkHasGzip() {
  854. static $haveGzip;
  855. if ( $haveGzip === null ) {
  856. $retval = null;
  857. wfShellExec( 'gzip -V', $retval );
  858. $haveGzip = ( $retval === 0 );
  859. }
  860. if ( !$haveGzip ) {
  861. $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
  862. }
  863. return $haveGzip;
  864. }
  865. /**
  866. * Check if $extName is a loaded PHP extension, will skip the
  867. * test whenever it is not loaded.
  868. *
  869. * @since 1.21
  870. */
  871. protected function checkPHPExtension( $extName ) {
  872. $loaded = extension_loaded( $extName );
  873. if ( !$loaded ) {
  874. $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
  875. }
  876. return $loaded;
  877. }
  878. /**
  879. * Asserts that an exception of the specified type occurs when running
  880. * the provided code.
  881. *
  882. * @since 1.21
  883. * @deprecated since 1.22 Use setExpectedException
  884. *
  885. * @param callable $code
  886. * @param string $expected
  887. * @param string $message
  888. */
  889. protected function assertException( $code, $expected = 'Exception', $message = '' ) {
  890. $pokemons = null;
  891. try {
  892. call_user_func( $code );
  893. } catch ( Exception $pokemons ) {
  894. // Gotta Catch 'Em All!
  895. }
  896. if ( $message === '' ) {
  897. $message = 'An exception of type "' . $expected . '" should have been thrown';
  898. }
  899. $this->assertInstanceOf( $expected, $pokemons, $message );
  900. }
  901. /**
  902. * Asserts that the given string is a valid HTML snippet.
  903. * Wraps the given string in the required top level tags and
  904. * then calls assertValidHtmlDocument().
  905. * The snippet is expected to be HTML 5.
  906. *
  907. * @since 1.23
  908. *
  909. * @note Will mark the test as skipped if the "tidy" module is not installed.
  910. * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially)
  911. * when automatic tidying is disabled.
  912. *
  913. * @param string $html An HTML snippet (treated as the contents of the body tag).
  914. */
  915. protected function assertValidHtmlSnippet( $html ) {
  916. $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>';
  917. $this->assertValidHtmlDocument( $html );
  918. }
  919. /**
  920. * Asserts that the given string is valid HTML document.
  921. *
  922. * @since 1.23
  923. *
  924. * @note Will mark the test as skipped if the "tidy" module is not installed.
  925. * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially)
  926. * when automatic tidying is disabled.
  927. *
  928. * @param string $html A complete HTML document
  929. */
  930. protected function assertValidHtmlDocument( $html ) {
  931. // Note: we only validate if the tidy PHP extension is available.
  932. // In case wgTidyInternal is false, MWTidy would fall back to the command line version
  933. // of tidy. In that case however, we can not reliably detect whether a failing validation
  934. // is due to malformed HTML, or caused by tidy not being installed as a command line tool.
  935. // That would cause all HTML assertions to fail on a system that has no tidy installed.
  936. if ( !$GLOBALS['wgTidyInternal'] ) {
  937. $this->markTestSkipped( 'Tidy extension not installed' );
  938. }
  939. $errorBuffer = '';
  940. MWTidy::checkErrors( $html, $errorBuffer );
  941. $allErrors = preg_split( '/[\r\n]+/', $errorBuffer );
  942. // Filter Tidy warnings which aren't useful for us.
  943. // Tidy eg. often cries about parameters missing which have actually
  944. // been deprecated since HTML4, thus we should not care about them.
  945. $errors = preg_grep(
  946. '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m',
  947. $allErrors, PREG_GREP_INVERT
  948. );
  949. $this->assertEmpty( $errors, implode( "\n", $errors ) );
  950. }
  951. }