MediaWikiTestCase.php 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312
  1. <?php
  2. use MediaWiki\Logger\LegacySpi;
  3. use MediaWiki\Logger\LoggerFactory;
  4. use MediaWiki\Logger\MonologSpi;
  5. use MediaWiki\MediaWikiServices;
  6. use Psr\Log\LoggerInterface;
  7. use Wikimedia\Rdbms\IDatabase;
  8. use Wikimedia\Rdbms\IMaintainableDatabase;
  9. use Wikimedia\Rdbms\Database;
  10. use Wikimedia\TestingAccessWrapper;
  11. /**
  12. * @since 1.18
  13. */
  14. abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
  15. use MediaWikiCoversValidator;
  16. use PHPUnit4And6Compat;
  17. /**
  18. * The original service locator. This is overridden during setUp().
  19. *
  20. * @var MediaWikiServices|null
  21. */
  22. private static $originalServices;
  23. /**
  24. * The local service locator, created during setUp().
  25. * @var MediaWikiServices
  26. */
  27. private $localServices;
  28. /**
  29. * $called tracks whether the setUp and tearDown method has been called.
  30. * class extending MediaWikiTestCase usually override setUp and tearDown
  31. * but forget to call the parent.
  32. *
  33. * The array format takes a method name as key and anything as a value.
  34. * By asserting the key exist, we know the child class has called the
  35. * parent.
  36. *
  37. * This property must be private, we do not want child to override it,
  38. * they should call the appropriate parent method instead.
  39. */
  40. private $called = [];
  41. /**
  42. * @var TestUser[]
  43. * @since 1.20
  44. */
  45. public static $users;
  46. /**
  47. * Primary database
  48. *
  49. * @var Database
  50. * @since 1.18
  51. */
  52. protected $db;
  53. /**
  54. * @var array
  55. * @since 1.19
  56. */
  57. protected $tablesUsed = []; // tables with data
  58. private static $useTemporaryTables = true;
  59. private static $reuseDB = false;
  60. private static $dbSetup = false;
  61. private static $oldTablePrefix = '';
  62. /**
  63. * Original value of PHP's error_reporting setting.
  64. *
  65. * @var int
  66. */
  67. private $phpErrorLevel;
  68. /**
  69. * Holds the paths of temporary files/directories created through getNewTempFile,
  70. * and getNewTempDirectory
  71. *
  72. * @var array
  73. */
  74. private $tmpFiles = [];
  75. /**
  76. * Holds original values of MediaWiki configuration settings
  77. * to be restored in tearDown().
  78. * See also setMwGlobals().
  79. * @var array
  80. */
  81. private $mwGlobals = [];
  82. /**
  83. * Holds list of MediaWiki configuration settings to be unset in tearDown().
  84. * See also setMwGlobals().
  85. * @var array
  86. */
  87. private $mwGlobalsToUnset = [];
  88. /**
  89. * Holds original loggers which have been replaced by setLogger()
  90. * @var LoggerInterface[]
  91. */
  92. private $loggers = [];
  93. /**
  94. * The CLI arguments passed through from phpunit.php
  95. * @var array
  96. */
  97. private $cliArgs = [];
  98. /**
  99. * Table name prefixes. Oracle likes it shorter.
  100. */
  101. const DB_PREFIX = 'unittest_';
  102. const ORA_DB_PREFIX = 'ut_';
  103. /**
  104. * @var array
  105. * @since 1.18
  106. */
  107. protected $supportedDBs = [
  108. 'mysql',
  109. 'sqlite',
  110. 'postgres',
  111. 'oracle'
  112. ];
  113. public function __construct( $name = null, array $data = [], $dataName = '' ) {
  114. parent::__construct( $name, $data, $dataName );
  115. $this->backupGlobals = false;
  116. $this->backupStaticAttributes = false;
  117. }
  118. public function __destruct() {
  119. // Complain if self::setUp() was called, but not self::tearDown()
  120. // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
  121. if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
  122. throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
  123. }
  124. }
  125. public static function setUpBeforeClass() {
  126. parent::setUpBeforeClass();
  127. // Get the original service locator
  128. if ( !self::$originalServices ) {
  129. self::$originalServices = MediaWikiServices::getInstance();
  130. }
  131. }
  132. /**
  133. * Convenience method for getting an immutable test user
  134. *
  135. * @since 1.28
  136. *
  137. * @param string[] $groups Groups the test user should be in.
  138. * @return TestUser
  139. */
  140. public static function getTestUser( $groups = [] ) {
  141. return TestUserRegistry::getImmutableTestUser( $groups );
  142. }
  143. /**
  144. * Convenience method for getting a mutable test user
  145. *
  146. * @since 1.28
  147. *
  148. * @param string[] $groups Groups the test user should be added in.
  149. * @return TestUser
  150. */
  151. public static function getMutableTestUser( $groups = [] ) {
  152. return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
  153. }
  154. /**
  155. * Convenience method for getting an immutable admin test user
  156. *
  157. * @since 1.28
  158. *
  159. * @param string[] $groups Groups the test user should be added to.
  160. * @return TestUser
  161. */
  162. public static function getTestSysop() {
  163. return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
  164. }
  165. /**
  166. * Returns a WikiPage representing an existing page.
  167. *
  168. * @since 1.32
  169. *
  170. * @param Title|string|null $title
  171. * @return WikiPage
  172. * @throws MWException If this test cases's needsDB() method doesn't return true.
  173. * Test cases can use "@group Database" to enable database test support,
  174. * or list the tables under testing in $this->tablesUsed, or override the
  175. * needsDB() method.
  176. */
  177. protected function getExistingTestPage( $title = null ) {
  178. if ( !$this->needsDB() ) {
  179. throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
  180. ' method should return true. Use @group Database or $this->tablesUsed.' );
  181. }
  182. $title = ( $title === null ) ? 'UTPage' : $title;
  183. $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
  184. $page = WikiPage::factory( $title );
  185. if ( !$page->exists() ) {
  186. $user = self::getTestSysop()->getUser();
  187. $page->doEditContent(
  188. new WikitextContent( 'UTContent' ),
  189. 'UTPageSummary',
  190. EDIT_NEW | EDIT_SUPPRESS_RC,
  191. false,
  192. $user
  193. );
  194. }
  195. return $page;
  196. }
  197. /**
  198. * Returns a WikiPage representing a non-existing page.
  199. *
  200. * @since 1.32
  201. *
  202. * @param Title|string|null $title
  203. * @return WikiPage
  204. * @throws MWException If this test cases's needsDB() method doesn't return true.
  205. * Test cases can use "@group Database" to enable database test support,
  206. * or list the tables under testing in $this->tablesUsed, or override the
  207. * needsDB() method.
  208. */
  209. protected function getNonexistingTestPage( $title = null ) {
  210. if ( !$this->needsDB() ) {
  211. throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
  212. ' method should return true. Use @group Database or $this->tablesUsed.' );
  213. }
  214. $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
  215. $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
  216. $page = WikiPage::factory( $title );
  217. if ( $page->exists() ) {
  218. $page->doDeleteArticle( 'Testing' );
  219. }
  220. return $page;
  221. }
  222. /**
  223. * @deprecated since 1.32
  224. */
  225. public static function prepareServices( Config $bootstrapConfig ) {
  226. }
  227. /**
  228. * Create a config suitable for testing, based on a base config, default overrides,
  229. * and custom overrides.
  230. *
  231. * @param Config|null $baseConfig
  232. * @param Config|null $customOverrides
  233. *
  234. * @return Config
  235. */
  236. private static function makeTestConfig(
  237. Config $baseConfig = null,
  238. Config $customOverrides = null
  239. ) {
  240. $defaultOverrides = new HashConfig();
  241. if ( !$baseConfig ) {
  242. $baseConfig = self::$originalServices->getBootstrapConfig();
  243. }
  244. /* Some functions require some kind of caching, and will end up using the db,
  245. * which we can't allow, as that would open a new connection for mysql.
  246. * Replace with a HashBag. They would not be going to persist anyway.
  247. */
  248. $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ];
  249. $objectCaches = [
  250. CACHE_DB => $hashCache,
  251. CACHE_ACCEL => $hashCache,
  252. CACHE_MEMCACHED => $hashCache,
  253. 'apc' => $hashCache,
  254. 'apcu' => $hashCache,
  255. 'wincache' => $hashCache,
  256. ] + $baseConfig->get( 'ObjectCaches' );
  257. $defaultOverrides->set( 'ObjectCaches', $objectCaches );
  258. $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
  259. $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] );
  260. // Use a fast hash algorithm to hash passwords.
  261. $defaultOverrides->set( 'PasswordDefault', 'A' );
  262. $testConfig = $customOverrides
  263. ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
  264. : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
  265. return $testConfig;
  266. }
  267. /**
  268. * @param ConfigFactory $oldFactory
  269. * @param Config[] $configurations
  270. *
  271. * @return Closure
  272. */
  273. private static function makeTestConfigFactoryInstantiator(
  274. ConfigFactory $oldFactory,
  275. array $configurations
  276. ) {
  277. return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
  278. $factory = new ConfigFactory();
  279. // clone configurations from $oldFactory that are not overwritten by $configurations
  280. $namesToClone = array_diff(
  281. $oldFactory->getConfigNames(),
  282. array_keys( $configurations )
  283. );
  284. foreach ( $namesToClone as $name ) {
  285. $factory->register( $name, $oldFactory->makeConfig( $name ) );
  286. }
  287. foreach ( $configurations as $name => $config ) {
  288. $factory->register( $name, $config );
  289. }
  290. return $factory;
  291. };
  292. }
  293. /**
  294. * Resets some non-service singleton instances and other static caches. It's not necessary to
  295. * reset services here.
  296. */
  297. public static function resetNonServiceCaches() {
  298. global $wgRequest, $wgJobClasses;
  299. foreach ( $wgJobClasses as $type => $class ) {
  300. JobQueueGroup::singleton()->get( $type )->delete();
  301. }
  302. JobQueueGroup::destroySingletons();
  303. ObjectCache::clear();
  304. FileBackendGroup::destroySingleton();
  305. DeferredUpdates::clearPendingUpdates();
  306. // TODO: move global state into MediaWikiServices
  307. RequestContext::resetMain();
  308. if ( session_id() !== '' ) {
  309. session_write_close();
  310. session_id( '' );
  311. }
  312. $wgRequest = new FauxRequest();
  313. MediaWiki\Session\SessionManager::resetCache();
  314. }
  315. public function run( PHPUnit_Framework_TestResult $result = null ) {
  316. if ( $result instanceof MediaWikiTestResult ) {
  317. $this->cliArgs = $result->getMediaWikiCliArgs();
  318. }
  319. $this->overrideMwServices();
  320. if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) {
  321. throw new Exception(
  322. get_class( $this ) . ' apparently needsDB but is not in the Database group'
  323. );
  324. }
  325. $needsResetDB = false;
  326. if ( !self::$dbSetup || $this->needsDB() ) {
  327. // set up a DB connection for this test to use
  328. self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
  329. self::$reuseDB = $this->getCliArg( 'reuse-db' );
  330. $this->db = wfGetDB( DB_MASTER );
  331. $this->checkDbIsSupported();
  332. if ( !self::$dbSetup ) {
  333. $this->setupAllTestDBs();
  334. $this->addCoreDBData();
  335. }
  336. // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
  337. // is available in subclass's setUpBeforeClass() and setUp() methods.
  338. // This would also remove the need for the HACK that is oncePerClass().
  339. if ( $this->oncePerClass() ) {
  340. $this->setUpSchema( $this->db );
  341. $this->resetDB( $this->db, $this->tablesUsed );
  342. $this->addDBDataOnce();
  343. }
  344. $this->addDBData();
  345. $needsResetDB = true;
  346. }
  347. parent::run( $result );
  348. if ( $needsResetDB ) {
  349. $this->resetDB( $this->db, $this->tablesUsed );
  350. }
  351. self::restoreMwServices();
  352. $this->localServices = null;
  353. }
  354. /**
  355. * @return bool
  356. */
  357. private function oncePerClass() {
  358. // Remember current test class in the database connection,
  359. // so we know when we need to run addData.
  360. $class = static::class;
  361. $first = !isset( $this->db->_hasDataForTestClass )
  362. || $this->db->_hasDataForTestClass !== $class;
  363. $this->db->_hasDataForTestClass = $class;
  364. return $first;
  365. }
  366. /**
  367. * @since 1.21
  368. *
  369. * @return bool
  370. */
  371. public function usesTemporaryTables() {
  372. return self::$useTemporaryTables;
  373. }
  374. /**
  375. * Obtains a new temporary file name
  376. *
  377. * The obtained filename is enlisted to be removed upon tearDown
  378. *
  379. * @since 1.20
  380. *
  381. * @return string Absolute name of the temporary file
  382. */
  383. protected function getNewTempFile() {
  384. $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
  385. $this->tmpFiles[] = $fileName;
  386. return $fileName;
  387. }
  388. /**
  389. * obtains a new temporary directory
  390. *
  391. * The obtained directory is enlisted to be removed (recursively with all its contained
  392. * files) upon tearDown.
  393. *
  394. * @since 1.20
  395. *
  396. * @return string Absolute name of the temporary directory
  397. */
  398. protected function getNewTempDirectory() {
  399. // Starting of with a temporary /file/.
  400. $fileName = $this->getNewTempFile();
  401. // Converting the temporary /file/ to a /directory/
  402. // The following is not atomic, but at least we now have a single place,
  403. // where temporary directory creation is bundled and can be improved
  404. unlink( $fileName );
  405. $this->assertTrue( wfMkdirParents( $fileName ) );
  406. return $fileName;
  407. }
  408. protected function setUp() {
  409. parent::setUp();
  410. $this->called['setUp'] = true;
  411. $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
  412. // Cleaning up temporary files
  413. foreach ( $this->tmpFiles as $fileName ) {
  414. if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
  415. unlink( $fileName );
  416. } elseif ( is_dir( $fileName ) ) {
  417. wfRecursiveRemoveDir( $fileName );
  418. }
  419. }
  420. if ( $this->needsDB() && $this->db ) {
  421. // Clean up open transactions
  422. while ( $this->db->trxLevel() > 0 ) {
  423. $this->db->rollback( __METHOD__, 'flush' );
  424. }
  425. // Check for unsafe queries
  426. if ( $this->db->getType() === 'mysql' ) {
  427. $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ );
  428. }
  429. }
  430. // Reset all caches between tests.
  431. self::resetNonServiceCaches();
  432. // XXX: reset maintenance triggers
  433. // Hook into period lag checks which often happen in long-running scripts
  434. $lbFactory = $this->localServices->getDBLoadBalancerFactory();
  435. Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() );
  436. ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
  437. }
  438. protected function addTmpFiles( $files ) {
  439. $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
  440. }
  441. protected function tearDown() {
  442. global $wgRequest, $wgSQLMode;
  443. $status = ob_get_status();
  444. if ( isset( $status['name'] ) &&
  445. $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
  446. ) {
  447. ob_end_flush();
  448. }
  449. $this->called['tearDown'] = true;
  450. // Cleaning up temporary files
  451. foreach ( $this->tmpFiles as $fileName ) {
  452. if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
  453. unlink( $fileName );
  454. } elseif ( is_dir( $fileName ) ) {
  455. wfRecursiveRemoveDir( $fileName );
  456. }
  457. }
  458. if ( $this->needsDB() && $this->db ) {
  459. // Clean up open transactions
  460. while ( $this->db->trxLevel() > 0 ) {
  461. $this->db->rollback( __METHOD__, 'flush' );
  462. }
  463. if ( $this->db->getType() === 'mysql' ) {
  464. $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ),
  465. __METHOD__ );
  466. }
  467. }
  468. // Restore mw globals
  469. foreach ( $this->mwGlobals as $key => $value ) {
  470. $GLOBALS[$key] = $value;
  471. }
  472. foreach ( $this->mwGlobalsToUnset as $value ) {
  473. unset( $GLOBALS[$value] );
  474. }
  475. if (
  476. array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
  477. in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
  478. ) {
  479. $this->resetNamespaces();
  480. }
  481. $this->mwGlobals = [];
  482. $this->mwGlobalsToUnset = [];
  483. $this->restoreLoggers();
  484. // TODO: move global state into MediaWikiServices
  485. RequestContext::resetMain();
  486. if ( session_id() !== '' ) {
  487. session_write_close();
  488. session_id( '' );
  489. }
  490. $wgRequest = new FauxRequest();
  491. MediaWiki\Session\SessionManager::resetCache();
  492. MediaWiki\Auth\AuthManager::resetCache();
  493. $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
  494. if ( $phpErrorLevel !== $this->phpErrorLevel ) {
  495. ini_set( 'error_reporting', $this->phpErrorLevel );
  496. $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
  497. $newHex = strtoupper( dechex( $phpErrorLevel ) );
  498. $message = "PHP error_reporting setting was left dirty: "
  499. . "was 0x$oldHex before test, 0x$newHex after test!";
  500. $this->fail( $message );
  501. }
  502. parent::tearDown();
  503. }
  504. /**
  505. * Make sure MediaWikiTestCase extending classes have called their
  506. * parent setUp method
  507. *
  508. * With strict coverage activated in PHP_CodeCoverage, this test would be
  509. * marked as risky without the following annotation (T152923).
  510. * @coversNothing
  511. */
  512. final public function testMediaWikiTestCaseParentSetupCalled() {
  513. $this->assertArrayHasKey( 'setUp', $this->called,
  514. static::class . '::setUp() must call parent::setUp()'
  515. );
  516. }
  517. /**
  518. * Sets a service, maintaining a stashed version of the previous service to be
  519. * restored in tearDown
  520. *
  521. * @since 1.27
  522. *
  523. * @param string $name
  524. * @param object $object
  525. */
  526. protected function setService( $name, $object ) {
  527. if ( !$this->localServices ) {
  528. throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
  529. }
  530. if ( $this->localServices !== MediaWikiServices::getInstance() ) {
  531. throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
  532. . 'instance has been replaced by test code.' );
  533. }
  534. $this->localServices->disableService( $name );
  535. $this->localServices->redefineService(
  536. $name,
  537. function () use ( $object ) {
  538. return $object;
  539. }
  540. );
  541. if ( $name === 'ContentLanguage' ) {
  542. $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
  543. }
  544. }
  545. /**
  546. * Sets a global, maintaining a stashed version of the previous global to be
  547. * restored in tearDown
  548. *
  549. * The key is added to the array of globals that will be reset afterwards
  550. * in the tearDown().
  551. *
  552. * @par Example
  553. * @code
  554. * protected function setUp() {
  555. * $this->setMwGlobals( 'wgRestrictStuff', true );
  556. * }
  557. *
  558. * function testFoo() {}
  559. *
  560. * function testBar() {}
  561. * $this->assertTrue( self::getX()->doStuff() );
  562. *
  563. * $this->setMwGlobals( 'wgRestrictStuff', false );
  564. * $this->assertTrue( self::getX()->doStuff() );
  565. * }
  566. *
  567. * function testQuux() {}
  568. * @endcode
  569. *
  570. * @param array|string $pairs Key to the global variable, or an array
  571. * of key/value pairs.
  572. * @param mixed|null $value Value to set the global to (ignored
  573. * if an array is given as first argument).
  574. *
  575. * @note To allow changes to global variables to take effect on global service instances,
  576. * call overrideMwServices().
  577. *
  578. * @since 1.21
  579. */
  580. protected function setMwGlobals( $pairs, $value = null ) {
  581. if ( is_string( $pairs ) ) {
  582. $pairs = [ $pairs => $value ];
  583. }
  584. if ( isset( $pairs['wgContLang'] ) ) {
  585. throw new MWException(
  586. 'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
  587. );
  588. }
  589. $this->doSetMwGlobals( $pairs, $value );
  590. }
  591. /**
  592. * An internal method that allows setService() to set globals that tests are not supposed to
  593. * touch.
  594. */
  595. private function doSetMwGlobals( $pairs, $value = null ) {
  596. $this->stashMwGlobals( array_keys( $pairs ) );
  597. foreach ( $pairs as $key => $value ) {
  598. $GLOBALS[$key] = $value;
  599. }
  600. if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
  601. $this->resetNamespaces();
  602. }
  603. }
  604. /**
  605. * Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
  606. * Otherwise old namespace data will lurk and cause bugs.
  607. */
  608. private function resetNamespaces() {
  609. if ( !$this->localServices ) {
  610. throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
  611. }
  612. if ( $this->localServices !== MediaWikiServices::getInstance() ) {
  613. throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
  614. . 'instance has been replaced by test code.' );
  615. }
  616. MWNamespace::clearCaches();
  617. Language::clearCaches();
  618. // We can't have the TitleFormatter holding on to an old Language object either
  619. // @todo We shouldn't need to reset all the aliases here.
  620. $this->localServices->resetServiceForTesting( 'TitleFormatter' );
  621. $this->localServices->resetServiceForTesting( 'TitleParser' );
  622. $this->localServices->resetServiceForTesting( '_MediaWikiTitleCodec' );
  623. }
  624. /**
  625. * Check if we can back up a value by performing a shallow copy.
  626. * Values which fail this test are copied recursively.
  627. *
  628. * @param mixed $value
  629. * @return bool True if a shallow copy will do; false if a deep copy
  630. * is required.
  631. */
  632. private static function canShallowCopy( $value ) {
  633. if ( is_scalar( $value ) || $value === null ) {
  634. return true;
  635. }
  636. if ( is_array( $value ) ) {
  637. foreach ( $value as $subValue ) {
  638. if ( !is_scalar( $subValue ) && $subValue !== null ) {
  639. return false;
  640. }
  641. }
  642. return true;
  643. }
  644. return false;
  645. }
  646. /**
  647. * Stashes the global, will be restored in tearDown()
  648. *
  649. * Individual test functions may override globals through the setMwGlobals() function
  650. * or directly. When directly overriding globals their keys should first be passed to this
  651. * method in setUp to avoid breaking global state for other tests
  652. *
  653. * That way all other tests are executed with the same settings (instead of using the
  654. * unreliable local settings for most tests and fix it only for some tests).
  655. *
  656. * @param array|string $globalKeys Key to the global variable, or an array of keys.
  657. *
  658. * @note To allow changes to global variables to take effect on global service instances,
  659. * call overrideMwServices().
  660. *
  661. * @since 1.23
  662. */
  663. protected function stashMwGlobals( $globalKeys ) {
  664. if ( is_string( $globalKeys ) ) {
  665. $globalKeys = [ $globalKeys ];
  666. }
  667. foreach ( $globalKeys as $globalKey ) {
  668. // NOTE: make sure we only save the global once or a second call to
  669. // setMwGlobals() on the same global would override the original
  670. // value.
  671. if (
  672. !array_key_exists( $globalKey, $this->mwGlobals ) &&
  673. !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
  674. ) {
  675. if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
  676. $this->mwGlobalsToUnset[$globalKey] = $globalKey;
  677. continue;
  678. }
  679. // NOTE: we serialize then unserialize the value in case it is an object
  680. // this stops any objects being passed by reference. We could use clone
  681. // and if is_object but this does account for objects within objects!
  682. if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
  683. $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
  684. } elseif (
  685. // Many MediaWiki types are safe to clone. These are the
  686. // ones that are most commonly stashed.
  687. $GLOBALS[$globalKey] instanceof Language ||
  688. $GLOBALS[$globalKey] instanceof User ||
  689. $GLOBALS[$globalKey] instanceof FauxRequest
  690. ) {
  691. $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
  692. } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
  693. // Serializing Closure only gives a warning on HHVM while
  694. // it throws an Exception on Zend.
  695. // Workaround for https://github.com/facebook/hhvm/issues/6206
  696. $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
  697. } else {
  698. try {
  699. $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
  700. } catch ( Exception $e ) {
  701. $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
  702. }
  703. }
  704. }
  705. }
  706. }
  707. /**
  708. * @param mixed $var
  709. * @param int $maxDepth
  710. *
  711. * @return bool
  712. */
  713. private function containsClosure( $var, $maxDepth = 15 ) {
  714. if ( $var instanceof Closure ) {
  715. return true;
  716. }
  717. if ( !is_array( $var ) || $maxDepth === 0 ) {
  718. return false;
  719. }
  720. foreach ( $var as $value ) {
  721. if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
  722. return true;
  723. }
  724. }
  725. return false;
  726. }
  727. /**
  728. * Merges the given values into a MW global array variable.
  729. * Useful for setting some entries in a configuration array, instead of
  730. * setting the entire array.
  731. *
  732. * @param string $name The name of the global, as in wgFooBar
  733. * @param array $values The array containing the entries to set in that global
  734. *
  735. * @throws MWException If the designated global is not an array.
  736. *
  737. * @note To allow changes to global variables to take effect on global service instances,
  738. * call overrideMwServices().
  739. *
  740. * @since 1.21
  741. */
  742. protected function mergeMwGlobalArrayValue( $name, $values ) {
  743. if ( !isset( $GLOBALS[$name] ) ) {
  744. $merged = $values;
  745. } else {
  746. if ( !is_array( $GLOBALS[$name] ) ) {
  747. throw new MWException( "MW global $name is not an array." );
  748. }
  749. // NOTE: do not use array_merge, it screws up for numeric keys.
  750. $merged = $GLOBALS[$name];
  751. foreach ( $values as $k => $v ) {
  752. $merged[$k] = $v;
  753. }
  754. }
  755. $this->setMwGlobals( $name, $merged );
  756. }
  757. /**
  758. * Stashes the global instance of MediaWikiServices, and installs a new one,
  759. * allowing test cases to override settings and services.
  760. * The previous instance of MediaWikiServices will be restored on tearDown.
  761. *
  762. * @since 1.27
  763. *
  764. * @param Config|null $configOverrides Configuration overrides for the new MediaWikiServices
  765. * instance.
  766. * @param callable[] $services An associative array of services to re-define. Keys are service
  767. * names, values are callables.
  768. *
  769. * @return MediaWikiServices
  770. * @throws MWException
  771. */
  772. protected function overrideMwServices(
  773. Config $configOverrides = null, array $services = []
  774. ) {
  775. $newInstance = self::installMockMwServices( $configOverrides );
  776. if ( $this->localServices ) {
  777. $this->localServices->destroy();
  778. }
  779. $this->localServices = $newInstance;
  780. foreach ( $services as $name => $callback ) {
  781. $newInstance->redefineService( $name, $callback );
  782. }
  783. return $newInstance;
  784. }
  785. /**
  786. * Creates a new "mock" MediaWikiServices instance, and installs it.
  787. * This effectively resets all cached states in services, with the exception of
  788. * the ConfigFactory and the DBLoadBalancerFactory service, which are inherited from
  789. * the original MediaWikiServices.
  790. *
  791. * @note The new original MediaWikiServices instance can later be restored by calling
  792. * restoreMwServices(). That original is determined by the first call to this method, or
  793. * by setUpBeforeClass, whichever is called first. The caller is responsible for managing
  794. * and, when appropriate, destroying any other MediaWikiServices instances that may get
  795. * replaced when calling this method.
  796. *
  797. * @param Config|null $configOverrides Configuration overrides for the new MediaWikiServices
  798. * instance.
  799. *
  800. * @return MediaWikiServices the new mock service locator.
  801. */
  802. public static function installMockMwServices( Config $configOverrides = null ) {
  803. // Make sure we have the original service locator
  804. if ( !self::$originalServices ) {
  805. self::$originalServices = MediaWikiServices::getInstance();
  806. }
  807. if ( !$configOverrides ) {
  808. $configOverrides = new HashConfig();
  809. }
  810. $oldConfigFactory = self::$originalServices->getConfigFactory();
  811. $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory();
  812. $testConfig = self::makeTestConfig( null, $configOverrides );
  813. $newServices = new MediaWikiServices( $testConfig );
  814. // Load the default wiring from the specified files.
  815. // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
  816. $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
  817. $newServices->loadWiringFiles( $wiringFiles );
  818. // Provide a traditional hook point to allow extensions to configure services.
  819. Hooks::run( 'MediaWikiServices', [ $newServices ] );
  820. // Use bootstrap config for all configuration.
  821. // This allows config overrides via global variables to take effect.
  822. $bootstrapConfig = $newServices->getBootstrapConfig();
  823. $newServices->resetServiceForTesting( 'ConfigFactory' );
  824. $newServices->redefineService(
  825. 'ConfigFactory',
  826. self::makeTestConfigFactoryInstantiator(
  827. $oldConfigFactory,
  828. [ 'main' => $bootstrapConfig ]
  829. )
  830. );
  831. $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
  832. $newServices->redefineService(
  833. 'DBLoadBalancerFactory',
  834. function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
  835. return $oldLoadBalancerFactory;
  836. }
  837. );
  838. MediaWikiServices::forceGlobalInstance( $newServices );
  839. return $newServices;
  840. }
  841. /**
  842. * Restores the original, non-mock MediaWikiServices instance.
  843. * The previously active MediaWikiServices instance is destroyed,
  844. * if it is different from the original that is to be restored.
  845. *
  846. * @note this if for internal use by test framework code. It should never be
  847. * called from inside a test case, a data provider, or a setUp or tearDown method.
  848. *
  849. * @return bool true if the original service locator was restored,
  850. * false if there was nothing too do.
  851. */
  852. public static function restoreMwServices() {
  853. if ( !self::$originalServices ) {
  854. return false;
  855. }
  856. $currentServices = MediaWikiServices::getInstance();
  857. if ( self::$originalServices === $currentServices ) {
  858. return false;
  859. }
  860. MediaWikiServices::forceGlobalInstance( self::$originalServices );
  861. $currentServices->destroy();
  862. return true;
  863. }
  864. /**
  865. * @since 1.27
  866. * @param string|Language $lang
  867. */
  868. public function setUserLang( $lang ) {
  869. RequestContext::getMain()->setLanguage( $lang );
  870. $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
  871. }
  872. /**
  873. * @since 1.27
  874. * @param string|Language $lang
  875. */
  876. public function setContentLang( $lang ) {
  877. if ( $lang instanceof Language ) {
  878. $langCode = $lang->getCode();
  879. $langObj = $lang;
  880. } else {
  881. $langCode = $lang;
  882. $langObj = Language::factory( $langCode );
  883. }
  884. $this->setMwGlobals( 'wgLanguageCode', $langCode );
  885. $this->setService( 'ContentLanguage', $langObj );
  886. }
  887. /**
  888. * Alters $wgGroupPermissions for the duration of the test. Can be called
  889. * with an array, like
  890. * [ '*' => [ 'read' => false ], 'user' => [ 'read' => false ] ]
  891. * or three values to set a single permission, like
  892. * $this->setGroupPermissions( '*', 'read', false );
  893. *
  894. * @since 1.31
  895. * @param array|string $newPerms Either an array of permissions to change,
  896. * in which case the next two parameters are ignored; or a single string
  897. * identifying a group, to use with the next two parameters.
  898. * @param string|null $newKey
  899. * @param mixed|null $newValue
  900. */
  901. public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
  902. global $wgGroupPermissions;
  903. $this->stashMwGlobals( 'wgGroupPermissions' );
  904. if ( is_string( $newPerms ) ) {
  905. $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
  906. }
  907. foreach ( $newPerms as $group => $permissions ) {
  908. foreach ( $permissions as $key => $value ) {
  909. $wgGroupPermissions[$group][$key] = $value;
  910. }
  911. }
  912. }
  913. /**
  914. * Sets the logger for a specified channel, for the duration of the test.
  915. * @since 1.27
  916. * @param string $channel
  917. * @param LoggerInterface $logger
  918. */
  919. protected function setLogger( $channel, LoggerInterface $logger ) {
  920. // TODO: Once loggers are managed by MediaWikiServices, use
  921. // overrideMwServices() to set loggers.
  922. $provider = LoggerFactory::getProvider();
  923. $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
  924. $singletons = $wrappedProvider->singletons;
  925. if ( $provider instanceof MonologSpi ) {
  926. if ( !isset( $this->loggers[$channel] ) ) {
  927. $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
  928. }
  929. $singletons['loggers'][$channel] = $logger;
  930. } elseif ( $provider instanceof LegacySpi ) {
  931. if ( !isset( $this->loggers[$channel] ) ) {
  932. $this->loggers[$channel] = $singletons[$channel] ?? null;
  933. }
  934. $singletons[$channel] = $logger;
  935. } else {
  936. throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
  937. . ' is not implemented' );
  938. }
  939. $wrappedProvider->singletons = $singletons;
  940. }
  941. /**
  942. * Restores loggers replaced by setLogger().
  943. * @since 1.27
  944. */
  945. private function restoreLoggers() {
  946. $provider = LoggerFactory::getProvider();
  947. $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
  948. $singletons = $wrappedProvider->singletons;
  949. foreach ( $this->loggers as $channel => $logger ) {
  950. if ( $provider instanceof MonologSpi ) {
  951. if ( $logger === null ) {
  952. unset( $singletons['loggers'][$channel] );
  953. } else {
  954. $singletons['loggers'][$channel] = $logger;
  955. }
  956. } elseif ( $provider instanceof LegacySpi ) {
  957. if ( $logger === null ) {
  958. unset( $singletons[$channel] );
  959. } else {
  960. $singletons[$channel] = $logger;
  961. }
  962. }
  963. }
  964. $wrappedProvider->singletons = $singletons;
  965. $this->loggers = [];
  966. }
  967. /**
  968. * @return string
  969. * @since 1.18
  970. */
  971. public function dbPrefix() {
  972. return self::getTestPrefixFor( $this->db );
  973. }
  974. /**
  975. * @param IDatabase $db
  976. * @return string
  977. * @since 1.32
  978. */
  979. public static function getTestPrefixFor( IDatabase $db ) {
  980. return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
  981. }
  982. /**
  983. * @return bool
  984. * @since 1.18
  985. */
  986. public function needsDB() {
  987. // If the test says it uses database tables, it needs the database
  988. return $this->tablesUsed || $this->isTestInDatabaseGroup();
  989. }
  990. /**
  991. * @return bool
  992. * @since 1.32
  993. */
  994. protected function isTestInDatabaseGroup() {
  995. // If the test class says it belongs to the Database group, it needs the database.
  996. // NOTE: This ONLY checks for the group in the class level doc comment.
  997. $rc = new ReflectionClass( $this );
  998. return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
  999. }
  1000. /**
  1001. * Insert a new page.
  1002. *
  1003. * Should be called from addDBData().
  1004. *
  1005. * @since 1.25 ($namespace in 1.28)
  1006. * @param string|Title $pageName Page name or title
  1007. * @param string $text Page's content
  1008. * @param int|null $namespace Namespace id (name cannot already contain namespace)
  1009. * @param User|null $user If null, static::getTestSysop()->getUser() is used.
  1010. * @return array Title object and page id
  1011. * @throws MWException If this test cases's needsDB() method doesn't return true.
  1012. * Test cases can use "@group Database" to enable database test support,
  1013. * or list the tables under testing in $this->tablesUsed, or override the
  1014. * needsDB() method.
  1015. */
  1016. protected function insertPage(
  1017. $pageName,
  1018. $text = 'Sample page for unit test.',
  1019. $namespace = null,
  1020. User $user = null
  1021. ) {
  1022. if ( !$this->needsDB() ) {
  1023. throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
  1024. ' method should return true. Use @group Database or $this->tablesUsed.' );
  1025. }
  1026. if ( is_string( $pageName ) ) {
  1027. $title = Title::newFromText( $pageName, $namespace );
  1028. } else {
  1029. $title = $pageName;
  1030. }
  1031. if ( !$user ) {
  1032. $user = static::getTestSysop()->getUser();
  1033. }
  1034. $comment = __METHOD__ . ': Sample page for unit test.';
  1035. $page = WikiPage::factory( $title );
  1036. $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
  1037. return [
  1038. 'title' => $title,
  1039. 'id' => $page->getId(),
  1040. ];
  1041. }
  1042. /**
  1043. * Stub. If a test suite needs to add additional data to the database, it should
  1044. * implement this method and do so. This method is called once per test suite
  1045. * (i.e. once per class).
  1046. *
  1047. * Note data added by this method may be removed by resetDB() depending on
  1048. * the contents of $tablesUsed.
  1049. *
  1050. * To add additional data between test function runs, override prepareDB().
  1051. *
  1052. * @see addDBData()
  1053. * @see resetDB()
  1054. *
  1055. * @since 1.27
  1056. */
  1057. public function addDBDataOnce() {
  1058. }
  1059. /**
  1060. * Stub. Subclasses may override this to prepare the database.
  1061. * Called before every test run (test function or data set).
  1062. *
  1063. * @see addDBDataOnce()
  1064. * @see resetDB()
  1065. *
  1066. * @since 1.18
  1067. */
  1068. public function addDBData() {
  1069. }
  1070. /**
  1071. * @since 1.32
  1072. */
  1073. protected function addCoreDBData() {
  1074. if ( $this->db->getType() == 'oracle' ) {
  1075. # Insert 0 user to prevent FK violations
  1076. # Anonymous user
  1077. if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
  1078. $this->db->insert( 'user', [
  1079. 'user_id' => 0,
  1080. 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
  1081. }
  1082. # Insert 0 page to prevent FK violations
  1083. # Blank page
  1084. if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
  1085. $this->db->insert( 'page', [
  1086. 'page_id' => 0,
  1087. 'page_namespace' => 0,
  1088. 'page_title' => ' ',
  1089. 'page_restrictions' => null,
  1090. 'page_is_redirect' => 0,
  1091. 'page_is_new' => 0,
  1092. 'page_random' => 0,
  1093. 'page_touched' => $this->db->timestamp(),
  1094. 'page_latest' => 0,
  1095. 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
  1096. }
  1097. }
  1098. SiteStatsInit::doPlaceholderInit();
  1099. User::resetIdByNameCache();
  1100. // Make sysop user
  1101. $user = static::getTestSysop()->getUser();
  1102. // Make 1 page with 1 revision
  1103. $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
  1104. if ( $page->getId() == 0 ) {
  1105. $page->doEditContent(
  1106. new WikitextContent( 'UTContent' ),
  1107. 'UTPageSummary',
  1108. EDIT_NEW | EDIT_SUPPRESS_RC,
  1109. false,
  1110. $user
  1111. );
  1112. // an edit always attempt to purge backlink links such as history
  1113. // pages. That is unnecessary.
  1114. JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
  1115. // WikiPages::doEditUpdates randomly adds RC purges
  1116. JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
  1117. // doEditContent() probably started the session via
  1118. // User::loadFromSession(). Close it now.
  1119. if ( session_id() !== '' ) {
  1120. session_write_close();
  1121. session_id( '' );
  1122. }
  1123. }
  1124. }
  1125. /**
  1126. * Restores MediaWiki to using the table set (table prefix) it was using before
  1127. * setupTestDB() was called. Useful if we need to perform database operations
  1128. * after the test run has finished (such as saving logs or profiling info).
  1129. *
  1130. * This is called by phpunit/bootstrap.php after the last test.
  1131. *
  1132. * @since 1.21
  1133. */
  1134. public static function teardownTestDB() {
  1135. global $wgJobClasses;
  1136. if ( !self::$dbSetup ) {
  1137. return;
  1138. }
  1139. Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
  1140. foreach ( $wgJobClasses as $type => $class ) {
  1141. // Delete any jobs under the clone DB (or old prefix in other stores)
  1142. JobQueueGroup::singleton()->get( $type )->delete();
  1143. }
  1144. CloneDatabase::changePrefix( self::$oldTablePrefix );
  1145. self::$oldTablePrefix = false;
  1146. self::$dbSetup = false;
  1147. }
  1148. /**
  1149. * Setups a database with cloned tables using the given prefix.
  1150. *
  1151. * If reuseDB is true and certain conditions apply, it will just change the prefix.
  1152. * Otherwise, it will clone the tables and change the prefix.
  1153. *
  1154. * @param IMaintainableDatabase $db Database to use
  1155. * @param string|null $prefix Prefix to use for test tables. If not given, the prefix is determined
  1156. * automatically for $db.
  1157. * @return bool True if tables were cloned, false if only the prefix was changed
  1158. */
  1159. protected static function setupDatabaseWithTestPrefix(
  1160. IMaintainableDatabase $db,
  1161. $prefix = null
  1162. ) {
  1163. if ( $prefix === null ) {
  1164. $prefix = self::getTestPrefixFor( $db );
  1165. }
  1166. if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
  1167. $db->tablePrefix( $prefix );
  1168. return false;
  1169. }
  1170. if ( !isset( $db->_originalTablePrefix ) ) {
  1171. $oldPrefix = $db->tablePrefix();
  1172. if ( $oldPrefix === $prefix ) {
  1173. // table already has the correct prefix, but presumably no cloned tables
  1174. $oldPrefix = self::$oldTablePrefix;
  1175. }
  1176. $db->tablePrefix( $oldPrefix );
  1177. $tablesCloned = self::listTables( $db );
  1178. $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
  1179. $dbClone->useTemporaryTables( self::$useTemporaryTables );
  1180. $dbClone->cloneTableStructure();
  1181. $db->tablePrefix( $prefix );
  1182. $db->_originalTablePrefix = $oldPrefix;
  1183. }
  1184. return true;
  1185. }
  1186. /**
  1187. * Set up all test DBs
  1188. */
  1189. public function setupAllTestDBs() {
  1190. global $wgDBprefix;
  1191. self::$oldTablePrefix = $wgDBprefix;
  1192. $testPrefix = $this->dbPrefix();
  1193. // switch to a temporary clone of the database
  1194. self::setupTestDB( $this->db, $testPrefix );
  1195. if ( self::isUsingExternalStoreDB() ) {
  1196. self::setupExternalStoreTestDBs( $testPrefix );
  1197. }
  1198. // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
  1199. // *any* database connections to operate on live data.
  1200. CloneDatabase::changePrefix( $testPrefix );
  1201. }
  1202. /**
  1203. * Creates an empty skeleton of the wiki database by cloning its structure
  1204. * to equivalent tables using the given $prefix. Then sets MediaWiki to
  1205. * use the new set of tables (aka schema) instead of the original set.
  1206. *
  1207. * This is used to generate a dummy table set, typically consisting of temporary
  1208. * tables, that will be used by tests instead of the original wiki database tables.
  1209. *
  1210. * @since 1.21
  1211. *
  1212. * @note the original table prefix is stored in self::$oldTablePrefix. This is used
  1213. * by teardownTestDB() to return the wiki to using the original table set.
  1214. *
  1215. * @note this method only works when first called. Subsequent calls have no effect,
  1216. * even if using different parameters.
  1217. *
  1218. * @param Database $db The database connection
  1219. * @param string $prefix The prefix to use for the new table set (aka schema).
  1220. *
  1221. * @throws MWException If the database table prefix is already $prefix
  1222. */
  1223. public static function setupTestDB( Database $db, $prefix ) {
  1224. if ( self::$dbSetup ) {
  1225. return;
  1226. }
  1227. if ( $db->tablePrefix() === $prefix ) {
  1228. throw new MWException(
  1229. 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
  1230. }
  1231. // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
  1232. // and Database no longer use global state.
  1233. self::$dbSetup = true;
  1234. if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
  1235. return;
  1236. }
  1237. // Assuming this isn't needed for External Store database, and not sure if the procedure
  1238. // would be available there.
  1239. if ( $db->getType() == 'oracle' ) {
  1240. $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ );
  1241. }
  1242. Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
  1243. }
  1244. /**
  1245. * Clones the External Store database(s) for testing
  1246. *
  1247. * @param string|null $testPrefix Prefix for test tables. Will be determined automatically
  1248. * if not given.
  1249. */
  1250. protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
  1251. $connections = self::getExternalStoreDatabaseConnections();
  1252. foreach ( $connections as $dbw ) {
  1253. self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
  1254. }
  1255. }
  1256. /**
  1257. * Gets master database connections for all of the ExternalStoreDB
  1258. * stores configured in $wgDefaultExternalStore.
  1259. *
  1260. * @return Database[] Array of Database master connections
  1261. */
  1262. protected static function getExternalStoreDatabaseConnections() {
  1263. global $wgDefaultExternalStore;
  1264. /** @var ExternalStoreDB $externalStoreDB */
  1265. $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
  1266. $defaultArray = (array)$wgDefaultExternalStore;
  1267. $dbws = [];
  1268. foreach ( $defaultArray as $url ) {
  1269. if ( strpos( $url, 'DB://' ) === 0 ) {
  1270. list( $proto, $cluster ) = explode( '://', $url, 2 );
  1271. // Avoid getMaster() because setupDatabaseWithTestPrefix()
  1272. // requires Database instead of plain DBConnRef/IDatabase
  1273. $dbws[] = $externalStoreDB->getMaster( $cluster );
  1274. }
  1275. }
  1276. return $dbws;
  1277. }
  1278. /**
  1279. * Check whether ExternalStoreDB is being used
  1280. *
  1281. * @return bool True if it's being used
  1282. */
  1283. protected static function isUsingExternalStoreDB() {
  1284. global $wgDefaultExternalStore;
  1285. if ( !$wgDefaultExternalStore ) {
  1286. return false;
  1287. }
  1288. $defaultArray = (array)$wgDefaultExternalStore;
  1289. foreach ( $defaultArray as $url ) {
  1290. if ( strpos( $url, 'DB://' ) === 0 ) {
  1291. return true;
  1292. }
  1293. }
  1294. return false;
  1295. }
  1296. /**
  1297. * @throws LogicException if the given database connection is not a set up to use
  1298. * mock tables.
  1299. *
  1300. * @since 1.31 this is no longer private.
  1301. */
  1302. protected function ensureMockDatabaseConnection( IDatabase $db ) {
  1303. if ( $db->tablePrefix() !== $this->dbPrefix() ) {
  1304. throw new LogicException(
  1305. 'Trying to delete mock tables, but table prefix does not indicate a mock database.'
  1306. );
  1307. }
  1308. }
  1309. private static $schemaOverrideDefaults = [
  1310. 'scripts' => [],
  1311. 'create' => [],
  1312. 'drop' => [],
  1313. 'alter' => [],
  1314. ];
  1315. /**
  1316. * Stub. If a test suite needs to test against a specific database schema, it should
  1317. * override this method and return the appropriate information from it.
  1318. *
  1319. * @param IMaintainableDatabase $db The DB connection to use for the mock schema.
  1320. * May be used to check the current state of the schema, to determine what
  1321. * overrides are needed.
  1322. *
  1323. * @return array An associative array with the following fields:
  1324. * - 'scripts': any SQL scripts to run. If empty or not present, schema overrides are skipped.
  1325. * - 'create': A list of tables created (may or may not exist in the original schema).
  1326. * - 'drop': A list of tables dropped (expected to be present in the original schema).
  1327. * - 'alter': A list of tables altered (expected to be present in the original schema).
  1328. */
  1329. protected function getSchemaOverrides( IMaintainableDatabase $db ) {
  1330. return [];
  1331. }
  1332. /**
  1333. * Undoes the specified schema overrides..
  1334. * Called once per test class, just before addDataOnce().
  1335. *
  1336. * @param IMaintainableDatabase $db
  1337. * @param array $oldOverrides
  1338. */
  1339. private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
  1340. $this->ensureMockDatabaseConnection( $db );
  1341. $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
  1342. $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
  1343. // Drop tables that need to be restored or removed.
  1344. $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
  1345. // Restore tables that have been dropped or created or altered,
  1346. // if they exist in the original schema.
  1347. $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
  1348. $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
  1349. if ( $tablesToDrop ) {
  1350. $this->dropMockTables( $db, $tablesToDrop );
  1351. }
  1352. if ( $tablesToRestore ) {
  1353. $this->recloneMockTables( $db, $tablesToRestore );
  1354. }
  1355. }
  1356. /**
  1357. * Applies the schema overrides returned by getSchemaOverrides(),
  1358. * after undoing any previously applied schema overrides.
  1359. * Called once per test class, just before addDataOnce().
  1360. */
  1361. private function setUpSchema( IMaintainableDatabase $db ) {
  1362. // Undo any active overrides.
  1363. $oldOverrides = $db->_schemaOverrides ?? self::$schemaOverrideDefaults;
  1364. if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
  1365. $this->undoSchemaOverrides( $db, $oldOverrides );
  1366. }
  1367. // Determine new overrides.
  1368. $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
  1369. $extraKeys = array_diff(
  1370. array_keys( $overrides ),
  1371. array_keys( self::$schemaOverrideDefaults )
  1372. );
  1373. if ( $extraKeys ) {
  1374. throw new InvalidArgumentException(
  1375. 'Schema override contains extra keys: ' . var_export( $extraKeys, true )
  1376. );
  1377. }
  1378. if ( !$overrides['scripts'] ) {
  1379. // no scripts to run
  1380. return;
  1381. }
  1382. if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
  1383. throw new InvalidArgumentException(
  1384. 'Schema override scripts given, but no tables are declared to be '
  1385. . 'created, dropped or altered.'
  1386. );
  1387. }
  1388. $this->ensureMockDatabaseConnection( $db );
  1389. // Drop the tables that will be created by the schema scripts.
  1390. $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
  1391. $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
  1392. if ( $tablesToDrop ) {
  1393. $this->dropMockTables( $db, $tablesToDrop );
  1394. }
  1395. // Run schema override scripts.
  1396. foreach ( $overrides['scripts'] as $script ) {
  1397. $db->sourceFile(
  1398. $script,
  1399. null,
  1400. null,
  1401. __METHOD__,
  1402. function ( $cmd ) {
  1403. return $this->mungeSchemaUpdateQuery( $cmd );
  1404. }
  1405. );
  1406. }
  1407. $db->_schemaOverrides = $overrides;
  1408. }
  1409. private function mungeSchemaUpdateQuery( $cmd ) {
  1410. return self::$useTemporaryTables
  1411. ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
  1412. : $cmd;
  1413. }
  1414. /**
  1415. * Drops the given mock tables.
  1416. *
  1417. * @param IMaintainableDatabase $db
  1418. * @param array $tables
  1419. */
  1420. private function dropMockTables( IMaintainableDatabase $db, array $tables ) {
  1421. $this->ensureMockDatabaseConnection( $db );
  1422. foreach ( $tables as $tbl ) {
  1423. $tbl = $db->tableName( $tbl );
  1424. $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
  1425. }
  1426. }
  1427. /**
  1428. * Lists all tables in the live database schema.
  1429. *
  1430. * @param IMaintainableDatabase $db
  1431. * @param string $prefix Either 'prefixed' or 'unprefixed'
  1432. * @return array
  1433. */
  1434. private function listOriginalTables( IMaintainableDatabase $db, $prefix = 'prefixed' ) {
  1435. if ( !isset( $db->_originalTablePrefix ) ) {
  1436. throw new LogicException( 'No original table prefix know, cannot list tables!' );
  1437. }
  1438. $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
  1439. if ( $prefix === 'unprefixed' ) {
  1440. $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
  1441. $originalTables = array_map(
  1442. function ( $pt ) use ( $originalPrefixRegex ) {
  1443. return preg_replace( $originalPrefixRegex, '', $pt );
  1444. },
  1445. $originalTables
  1446. );
  1447. }
  1448. return $originalTables;
  1449. }
  1450. /**
  1451. * Re-clones the given mock tables to restore them based on the live database schema.
  1452. * The tables listed in $tables are expected to currently not exist, so dropMockTables()
  1453. * should be called first.
  1454. *
  1455. * @param IMaintainableDatabase $db
  1456. * @param array $tables
  1457. */
  1458. private function recloneMockTables( IMaintainableDatabase $db, array $tables ) {
  1459. $this->ensureMockDatabaseConnection( $db );
  1460. if ( !isset( $db->_originalTablePrefix ) ) {
  1461. throw new LogicException( 'No original table prefix know, cannot restore tables!' );
  1462. }
  1463. $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
  1464. $tables = array_intersect( $tables, $originalTables );
  1465. $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
  1466. $dbClone->useTemporaryTables( self::$useTemporaryTables );
  1467. $dbClone->cloneTableStructure();
  1468. }
  1469. /**
  1470. * Empty all tables so they can be repopulated for tests
  1471. *
  1472. * @param Database $db|null Database to reset
  1473. * @param array $tablesUsed Tables to reset
  1474. */
  1475. private function resetDB( $db, $tablesUsed ) {
  1476. if ( $db ) {
  1477. $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
  1478. $pageTables = [
  1479. 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
  1480. 'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
  1481. ];
  1482. $coreDBDataTables = array_merge( $userTables, $pageTables );
  1483. // If any of the user or page tables were marked as used, we should clear all of them.
  1484. if ( array_intersect( $tablesUsed, $userTables ) ) {
  1485. $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
  1486. TestUserRegistry::clear();
  1487. }
  1488. if ( array_intersect( $tablesUsed, $pageTables ) ) {
  1489. $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
  1490. }
  1491. // Postgres, Oracle, and MSSQL all use mwuser/pagecontent
  1492. // instead of user/text. But Postgres does not remap the
  1493. // table name in tableExists(), so we mark the real table
  1494. // names as being used.
  1495. if ( $db->getType() === 'postgres' ) {
  1496. if ( in_array( 'user', $tablesUsed ) ) {
  1497. $tablesUsed[] = 'mwuser';
  1498. }
  1499. if ( in_array( 'text', $tablesUsed ) ) {
  1500. $tablesUsed[] = 'pagecontent';
  1501. }
  1502. }
  1503. foreach ( $tablesUsed as $tbl ) {
  1504. $this->truncateTable( $tbl, $db );
  1505. }
  1506. if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
  1507. // Reset services that may contain information relating to the truncated tables
  1508. $this->overrideMwServices();
  1509. // Re-add core DB data that was deleted
  1510. $this->addCoreDBData();
  1511. }
  1512. }
  1513. }
  1514. /**
  1515. * Empties the given table and resets any auto-increment counters.
  1516. * Will also purge caches associated with some well known tables.
  1517. * If the table is not know, this method just returns.
  1518. *
  1519. * @param string $tableName
  1520. * @param IDatabase|null $db
  1521. */
  1522. protected function truncateTable( $tableName, IDatabase $db = null ) {
  1523. if ( !$db ) {
  1524. $db = $this->db;
  1525. }
  1526. if ( !$db->tableExists( $tableName ) ) {
  1527. return;
  1528. }
  1529. $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
  1530. if ( $truncate ) {
  1531. $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tableName ), __METHOD__ );
  1532. } else {
  1533. $db->delete( $tableName, '*', __METHOD__ );
  1534. }
  1535. if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) {
  1536. // Reset the table's sequence too.
  1537. $db->resetSequenceForTable( $tableName, __METHOD__ );
  1538. }
  1539. // re-initialize site_stats table
  1540. if ( $tableName === 'site_stats' ) {
  1541. SiteStatsInit::doPlaceholderInit();
  1542. }
  1543. }
  1544. private static function unprefixTable( &$tableName, $ind, $prefix ) {
  1545. $tableName = substr( $tableName, strlen( $prefix ) );
  1546. }
  1547. private static function isNotUnittest( $table ) {
  1548. return strpos( $table, self::DB_PREFIX ) !== 0;
  1549. }
  1550. /**
  1551. * @since 1.18
  1552. *
  1553. * @param IMaintainableDatabase $db
  1554. *
  1555. * @return array
  1556. */
  1557. public static function listTables( IMaintainableDatabase $db ) {
  1558. $prefix = $db->tablePrefix();
  1559. $tables = $db->listTables( $prefix, __METHOD__ );
  1560. if ( $db->getType() === 'mysql' ) {
  1561. static $viewListCache = null;
  1562. if ( $viewListCache === null ) {
  1563. $viewListCache = $db->listViews( null, __METHOD__ );
  1564. }
  1565. // T45571: cannot clone VIEWs under MySQL
  1566. $tables = array_diff( $tables, $viewListCache );
  1567. }
  1568. array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
  1569. // Don't duplicate test tables from the previous fataled run
  1570. $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
  1571. if ( $db->getType() == 'sqlite' ) {
  1572. $tables = array_flip( $tables );
  1573. // these are subtables of searchindex and don't need to be duped/dropped separately
  1574. unset( $tables['searchindex_content'] );
  1575. unset( $tables['searchindex_segdir'] );
  1576. unset( $tables['searchindex_segments'] );
  1577. $tables = array_flip( $tables );
  1578. }
  1579. return $tables;
  1580. }
  1581. /**
  1582. * Copy test data from one database connection to another.
  1583. *
  1584. * This should only be used for small data sets.
  1585. *
  1586. * @param IDatabase $source
  1587. * @param IDatabase $target
  1588. */
  1589. public function copyTestData( IDatabase $source, IDatabase $target ) {
  1590. $tables = self::listOriginalTables( $source, 'unprefixed' );
  1591. foreach ( $tables as $table ) {
  1592. $res = $source->select( $table, '*', [], __METHOD__ );
  1593. $allRows = [];
  1594. foreach ( $res as $row ) {
  1595. $allRows[] = (array)$row;
  1596. }
  1597. $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
  1598. }
  1599. }
  1600. /**
  1601. * @throws MWException
  1602. * @since 1.18
  1603. */
  1604. protected function checkDbIsSupported() {
  1605. if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
  1606. throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
  1607. }
  1608. }
  1609. /**
  1610. * @since 1.18
  1611. * @param string $offset
  1612. * @return mixed
  1613. */
  1614. public function getCliArg( $offset ) {
  1615. return $this->cliArgs[$offset] ?? null;
  1616. }
  1617. /**
  1618. * @since 1.18
  1619. * @param string $offset
  1620. * @param mixed $value
  1621. */
  1622. public function setCliArg( $offset, $value ) {
  1623. $this->cliArgs[$offset] = $value;
  1624. }
  1625. /**
  1626. * Don't throw a warning if $function is deprecated and called later
  1627. *
  1628. * @since 1.19
  1629. *
  1630. * @param string $function
  1631. */
  1632. public function hideDeprecated( $function ) {
  1633. Wikimedia\suppressWarnings();
  1634. wfDeprecated( $function );
  1635. Wikimedia\restoreWarnings();
  1636. }
  1637. /**
  1638. * Asserts that the given database query yields the rows given by $expectedRows.
  1639. * The expected rows should be given as indexed (not associative) arrays, with
  1640. * the values given in the order of the columns in the $fields parameter.
  1641. * Note that the rows are sorted by the columns given in $fields.
  1642. *
  1643. * @since 1.20
  1644. *
  1645. * @param string|array $table The table(s) to query
  1646. * @param string|array $fields The columns to include in the result (and to sort by)
  1647. * @param string|array $condition "where" condition(s)
  1648. * @param array $expectedRows An array of arrays giving the expected rows.
  1649. * @param array $options Options for the query
  1650. * @param array $join_conds Join conditions for the query
  1651. *
  1652. * @throws MWException If this test cases's needsDB() method doesn't return true.
  1653. * Test cases can use "@group Database" to enable database test support,
  1654. * or list the tables under testing in $this->tablesUsed, or override the
  1655. * needsDB() method.
  1656. */
  1657. protected function assertSelect(
  1658. $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
  1659. ) {
  1660. if ( !$this->needsDB() ) {
  1661. throw new MWException( 'When testing database state, the test cases\'s needDB()' .
  1662. ' method should return true. Use @group Database or $this->tablesUsed.' );
  1663. }
  1664. $db = wfGetDB( DB_REPLICA );
  1665. $res = $db->select(
  1666. $table,
  1667. $fields,
  1668. $condition,
  1669. wfGetCaller(),
  1670. $options + [ 'ORDER BY' => $fields ],
  1671. $join_conds
  1672. );
  1673. $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
  1674. $i = 0;
  1675. foreach ( $expectedRows as $expected ) {
  1676. $r = $res->fetchRow();
  1677. self::stripStringKeys( $r );
  1678. $i += 1;
  1679. $this->assertNotEmpty( $r, "row #$i missing" );
  1680. $this->assertEquals( $expected, $r, "row #$i mismatches" );
  1681. }
  1682. $r = $res->fetchRow();
  1683. self::stripStringKeys( $r );
  1684. $this->assertFalse( $r, "found extra row (after #$i)" );
  1685. }
  1686. /**
  1687. * Utility method taking an array of elements and wrapping
  1688. * each element in its own array. Useful for data providers
  1689. * that only return a single argument.
  1690. *
  1691. * @since 1.20
  1692. *
  1693. * @param array $elements
  1694. *
  1695. * @return array
  1696. */
  1697. protected function arrayWrap( array $elements ) {
  1698. return array_map(
  1699. function ( $element ) {
  1700. return [ $element ];
  1701. },
  1702. $elements
  1703. );
  1704. }
  1705. /**
  1706. * Assert that two arrays are equal. By default this means that both arrays need to hold
  1707. * the same set of values. Using additional arguments, order and associated key can also
  1708. * be set as relevant.
  1709. *
  1710. * @since 1.20
  1711. *
  1712. * @param array $expected
  1713. * @param array $actual
  1714. * @param bool $ordered If the order of the values should match
  1715. * @param bool $named If the keys should match
  1716. */
  1717. protected function assertArrayEquals( array $expected, array $actual,
  1718. $ordered = false, $named = false
  1719. ) {
  1720. if ( !$ordered ) {
  1721. $this->objectAssociativeSort( $expected );
  1722. $this->objectAssociativeSort( $actual );
  1723. }
  1724. if ( !$named ) {
  1725. $expected = array_values( $expected );
  1726. $actual = array_values( $actual );
  1727. }
  1728. call_user_func_array(
  1729. [ $this, 'assertEquals' ],
  1730. array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
  1731. );
  1732. }
  1733. /**
  1734. * Put each HTML element on its own line and then equals() the results
  1735. *
  1736. * Use for nicely formatting of PHPUnit diff output when comparing very
  1737. * simple HTML
  1738. *
  1739. * @since 1.20
  1740. *
  1741. * @param string $expected HTML on oneline
  1742. * @param string $actual HTML on oneline
  1743. * @param string $msg Optional message
  1744. */
  1745. protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
  1746. $expected = str_replace( '>', ">\n", $expected );
  1747. $actual = str_replace( '>', ">\n", $actual );
  1748. $this->assertEquals( $expected, $actual, $msg );
  1749. }
  1750. /**
  1751. * Does an associative sort that works for objects.
  1752. *
  1753. * @since 1.20
  1754. *
  1755. * @param array &$array
  1756. */
  1757. protected function objectAssociativeSort( array &$array ) {
  1758. uasort(
  1759. $array,
  1760. function ( $a, $b ) {
  1761. return serialize( $a ) <=> serialize( $b );
  1762. }
  1763. );
  1764. }
  1765. /**
  1766. * Utility function for eliminating all string keys from an array.
  1767. * Useful to turn a database result row as returned by fetchRow() into
  1768. * a pure indexed array.
  1769. *
  1770. * @since 1.20
  1771. *
  1772. * @param mixed &$r The array to remove string keys from.
  1773. */
  1774. protected static function stripStringKeys( &$r ) {
  1775. if ( !is_array( $r ) ) {
  1776. return;
  1777. }
  1778. foreach ( $r as $k => $v ) {
  1779. if ( is_string( $k ) ) {
  1780. unset( $r[$k] );
  1781. }
  1782. }
  1783. }
  1784. /**
  1785. * Asserts that the provided variable is of the specified
  1786. * internal type or equals the $value argument. This is useful
  1787. * for testing return types of functions that return a certain
  1788. * type or *value* when not set or on error.
  1789. *
  1790. * @since 1.20
  1791. *
  1792. * @param string $type
  1793. * @param mixed $actual
  1794. * @param mixed $value
  1795. * @param string $message
  1796. */
  1797. protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
  1798. if ( $actual === $value ) {
  1799. $this->assertTrue( true, $message );
  1800. } else {
  1801. $this->assertType( $type, $actual, $message );
  1802. }
  1803. }
  1804. /**
  1805. * Asserts the type of the provided value. This can be either
  1806. * in internal type such as boolean or integer, or a class or
  1807. * interface the value extends or implements.
  1808. *
  1809. * @since 1.20
  1810. *
  1811. * @param string $type
  1812. * @param mixed $actual
  1813. * @param string $message
  1814. */
  1815. protected function assertType( $type, $actual, $message = '' ) {
  1816. if ( class_exists( $type ) || interface_exists( $type ) ) {
  1817. $this->assertInstanceOf( $type, $actual, $message );
  1818. } else {
  1819. $this->assertInternalType( $type, $actual, $message );
  1820. }
  1821. }
  1822. /**
  1823. * Returns true if the given namespace defaults to Wikitext
  1824. * according to $wgNamespaceContentModels
  1825. *
  1826. * @param int $ns The namespace ID to check
  1827. *
  1828. * @return bool
  1829. * @since 1.21
  1830. */
  1831. protected function isWikitextNS( $ns ) {
  1832. global $wgNamespaceContentModels;
  1833. if ( isset( $wgNamespaceContentModels[$ns] ) ) {
  1834. return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
  1835. }
  1836. return true;
  1837. }
  1838. /**
  1839. * Returns the ID of a namespace that defaults to Wikitext.
  1840. *
  1841. * @throws MWException If there is none.
  1842. * @return int The ID of the wikitext Namespace
  1843. * @since 1.21
  1844. */
  1845. protected function getDefaultWikitextNS() {
  1846. global $wgNamespaceContentModels;
  1847. static $wikitextNS = null; // this is not going to change
  1848. if ( $wikitextNS !== null ) {
  1849. return $wikitextNS;
  1850. }
  1851. // quickly short out on most common case:
  1852. if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
  1853. return NS_MAIN;
  1854. }
  1855. // NOTE: prefer content namespaces
  1856. $namespaces = array_unique( array_merge(
  1857. MWNamespace::getContentNamespaces(),
  1858. [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
  1859. MWNamespace::getValidNamespaces()
  1860. ) );
  1861. $namespaces = array_diff( $namespaces, [
  1862. NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
  1863. ] );
  1864. $talk = array_filter( $namespaces, function ( $ns ) {
  1865. return MWNamespace::isTalk( $ns );
  1866. } );
  1867. // prefer non-talk pages
  1868. $namespaces = array_diff( $namespaces, $talk );
  1869. $namespaces = array_merge( $namespaces, $talk );
  1870. // check default content model of each namespace
  1871. foreach ( $namespaces as $ns ) {
  1872. if ( !isset( $wgNamespaceContentModels[$ns] ) ||
  1873. $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
  1874. ) {
  1875. $wikitextNS = $ns;
  1876. return $wikitextNS;
  1877. }
  1878. }
  1879. // give up
  1880. // @todo Inside a test, we could skip the test as incomplete.
  1881. // But frequently, this is used in fixture setup.
  1882. throw new MWException( "No namespace defaults to wikitext!" );
  1883. }
  1884. /**
  1885. * Check, if $wgDiff3 is set and ready to merge
  1886. * Will mark the calling test as skipped, if not ready
  1887. *
  1888. * @since 1.21
  1889. */
  1890. protected function markTestSkippedIfNoDiff3() {
  1891. global $wgDiff3;
  1892. # This check may also protect against code injection in
  1893. # case of broken installations.
  1894. Wikimedia\suppressWarnings();
  1895. $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
  1896. Wikimedia\restoreWarnings();
  1897. if ( !$haveDiff3 ) {
  1898. $this->markTestSkipped( "Skip test, since diff3 is not configured" );
  1899. }
  1900. }
  1901. /**
  1902. * Check if $extName is a loaded PHP extension, will skip the
  1903. * test whenever it is not loaded.
  1904. *
  1905. * @since 1.21
  1906. * @param string $extName
  1907. * @return bool
  1908. */
  1909. protected function checkPHPExtension( $extName ) {
  1910. $loaded = extension_loaded( $extName );
  1911. if ( !$loaded ) {
  1912. $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
  1913. }
  1914. return $loaded;
  1915. }
  1916. /**
  1917. * Skip the test if using the specified database type
  1918. *
  1919. * @param string $type Database type
  1920. * @since 1.32
  1921. */
  1922. protected function markTestSkippedIfDbType( $type ) {
  1923. if ( $this->db->getType() === $type ) {
  1924. $this->markTestSkipped( "The $type database type isn't supported for this test" );
  1925. }
  1926. }
  1927. /**
  1928. * Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit.
  1929. * @param string $buffer
  1930. * @return string
  1931. */
  1932. public static function wfResetOutputBuffersBarrier( $buffer ) {
  1933. return $buffer;
  1934. }
  1935. /**
  1936. * Create a temporary hook handler which will be reset by tearDown.
  1937. * This replaces other handlers for the same hook.
  1938. * @param string $hookName Hook name
  1939. * @param mixed $handler Value suitable for a hook handler
  1940. * @since 1.28
  1941. */
  1942. protected function setTemporaryHook( $hookName, $handler ) {
  1943. $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
  1944. }
  1945. /**
  1946. * Check whether file contains given data.
  1947. * @param string $fileName
  1948. * @param string $actualData
  1949. * @param bool $createIfMissing If true, and file does not exist, create it with given data
  1950. * and skip the test.
  1951. * @param string $msg
  1952. * @since 1.30
  1953. */
  1954. protected function assertFileContains(
  1955. $fileName,
  1956. $actualData,
  1957. $createIfMissing = false,
  1958. $msg = ''
  1959. ) {
  1960. if ( $createIfMissing ) {
  1961. if ( !file_exists( $fileName ) ) {
  1962. file_put_contents( $fileName, $actualData );
  1963. $this->markTestSkipped( 'Data file $fileName does not exist' );
  1964. }
  1965. } else {
  1966. self::assertFileExists( $fileName );
  1967. }
  1968. self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
  1969. }
  1970. /**
  1971. * Edits or creates a page/revision
  1972. * @param string $pageName Page title
  1973. * @param string $text Content of the page
  1974. * @param string $summary Optional summary string for the revision
  1975. * @param int $defaultNs Optional namespace id
  1976. * @return array Array as returned by WikiPage::doEditContent()
  1977. * @throws MWException If this test cases's needsDB() method doesn't return true.
  1978. * Test cases can use "@group Database" to enable database test support,
  1979. * or list the tables under testing in $this->tablesUsed, or override the
  1980. * needsDB() method.
  1981. */
  1982. protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
  1983. if ( !$this->needsDB() ) {
  1984. throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
  1985. ' method should return true. Use @group Database or $this->tablesUsed.' );
  1986. }
  1987. $title = Title::newFromText( $pageName, $defaultNs );
  1988. $page = WikiPage::factory( $title );
  1989. return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
  1990. }
  1991. /**
  1992. * Revision-deletes a revision.
  1993. *
  1994. * @param Revision|int $rev Revision to delete
  1995. * @param array $value Keys are Revision::DELETED_* flags. Values are 1 to set the bit, 0 to
  1996. * clear, -1 to leave alone. (All other values also clear the bit.)
  1997. * @param string $comment Deletion comment
  1998. */
  1999. protected function revisionDelete(
  2000. $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
  2001. ) {
  2002. if ( is_int( $rev ) ) {
  2003. $rev = Revision::newFromId( $rev );
  2004. }
  2005. RevisionDeleter::createList(
  2006. 'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
  2007. )->setVisibility( [
  2008. 'value' => $value,
  2009. 'comment' => $comment,
  2010. ] );
  2011. }
  2012. }