Deglobalization in EditPage.php
[lhc/web/wiklou.git] / tests / phpunit / MediaWikiTestCase.php
1 <?php
2
3 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
4 public $suite;
5 public $regex = '';
6 public $runDisabled = false;
7
8 /**
9 * @var Array of TestUser
10 */
11 public static $users;
12
13 /**
14 * @var DatabaseBase
15 */
16 protected $db;
17 protected $oldTablePrefix;
18 protected $useTemporaryTables = true;
19 protected $reuseDB = false;
20 protected $tablesUsed = array(); // tables with data
21
22 private static $dbSetup = false;
23
24 /**
25 * Holds the paths of temporary files/directories created through getNewTempFile,
26 * and getNewTempDirectory
27 *
28 * @var array
29 */
30 private $tmpfiles = array();
31
32 /**
33 * Holds original values of MediaWiki configuration settings
34 * to be restored in tearDown().
35 * See also setMwGlobal().
36 * @var array
37 */
38 private $mwGlobals = array();
39
40 /**
41 * Table name prefixes. Oracle likes it shorter.
42 */
43 const DB_PREFIX = 'unittest_';
44 const ORA_DB_PREFIX = 'ut_';
45
46 protected $supportedDBs = array(
47 'mysql',
48 'sqlite',
49 'postgres',
50 'oracle'
51 );
52
53 function __construct( $name = null, array $data = array(), $dataName = '' ) {
54 parent::__construct( $name, $data, $dataName );
55
56 $this->backupGlobals = false;
57 $this->backupStaticAttributes = false;
58 }
59
60 function run( PHPUnit_Framework_TestResult $result = NULL ) {
61 /* Some functions require some kind of caching, and will end up using the db,
62 * which we can't allow, as that would open a new connection for mysql.
63 * Replace with a HashBag. They would not be going to persist anyway.
64 */
65 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
66
67 if( $this->needsDB() ) {
68 global $wgDBprefix;
69
70 $this->useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
71 $this->reuseDB = $this->getCliArg('reuse-db');
72
73 $this->db = wfGetDB( DB_MASTER );
74
75 $this->checkDbIsSupported();
76
77 $this->oldTablePrefix = $wgDBprefix;
78
79 if( !self::$dbSetup ) {
80 $this->initDB();
81 self::$dbSetup = true;
82 }
83
84 $this->addCoreDBData();
85 $this->addDBData();
86
87 parent::run( $result );
88
89 $this->resetDB();
90 } else {
91 parent::run( $result );
92 }
93 }
94
95 /**
96 * obtains a new temporary file name
97 *
98 * The obtained filename is enlisted to be removed upon tearDown
99 *
100 * @returns string: absolute name of the temporary file
101 */
102 protected function getNewTempFile() {
103 $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
104 $this->tmpfiles[] = $fname;
105 return $fname;
106 }
107
108 /**
109 * obtains a new temporary directory
110 *
111 * The obtained directory is enlisted to be removed (recursively with all its contained
112 * files) upon tearDown.
113 *
114 * @returns string: absolute name of the temporary directory
115 */
116 protected function getNewTempDirectory() {
117 // Starting of with a temporary /file/.
118 $fname = $this->getNewTempFile();
119
120 // Converting the temporary /file/ to a /directory/
121 //
122 // The following is not atomic, but at least we now have a single place,
123 // where temporary directory creation is bundled and can be improved
124 unlink( $fname );
125 $this->assertTrue( wfMkdirParents( $fname ) );
126 return $fname;
127 }
128
129 /**
130 * setUp and tearDown should (where significant)
131 * happen in reverse order.
132 */
133 protected function setUp() {
134 parent::setUp();
135
136 /*
137 //@todo: global variables to restore for *every* test
138 array(
139 'wgLang',
140 'wgContLang',
141 'wgLanguageCode',
142 'wgUser',
143 'wgTitle',
144 );
145 */
146
147 // Cleaning up temporary files
148 foreach ( $this->tmpfiles as $fname ) {
149 if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
150 unlink( $fname );
151 } elseif ( is_dir( $fname ) ) {
152 wfRecursiveRemoveDir( $fname );
153 }
154 }
155
156 // Clean up open transactions
157 if ( $this->needsDB() && $this->db ) {
158 while( $this->db->trxLevel() > 0 ) {
159 $this->db->rollback();
160 }
161 }
162 }
163
164 protected function tearDown() {
165 // Cleaning up temporary files
166 foreach ( $this->tmpfiles as $fname ) {
167 if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
168 unlink( $fname );
169 } elseif ( is_dir( $fname ) ) {
170 wfRecursiveRemoveDir( $fname );
171 }
172 }
173
174 // Clean up open transactions
175 if ( $this->needsDB() && $this->db ) {
176 while( $this->db->trxLevel() > 0 ) {
177 $this->db->rollback();
178 }
179 }
180
181 // Restore mw globals
182 foreach ( $this->mwGlobals as $key => $value ) {
183 $GLOBALS[$key] = $value;
184 }
185 $this->mwGlobals = array();
186
187 parent::tearDown();
188 }
189
190 /**
191 * Individual test functions may override globals (either directly or through this
192 * setMwGlobals() function), however one must call this method at least once for
193 * each key within the setUp().
194 * That way the key is added to the array of globals that will be reset afterwards
195 * in the tearDown(). And, equally important, that way all other tests are executed
196 * with the same settings (instead of using the unreliable local settings for most
197 * tests and fix it only for some tests).
198 *
199 * @example
200 * <code>
201 * protected function setUp() {
202 * $this->setMwGlobals( 'wgRestrictStuff', true );
203 * }
204 *
205 * function testFoo() {}
206 *
207 * function testBar() {}
208 * $this->assertTrue( self::getX()->doStuff() );
209 *
210 * $this->setMwGlobals( 'wgRestrictStuff', false );
211 * $this->assertTrue( self::getX()->doStuff() );
212 * }
213 *
214 * function testQuux() {}
215 * </code>
216 *
217 * @param array|string $pairs Key to the global variable, or an array
218 * of key/value pairs.
219 * @param mixed $value Value to set the global to (ignored
220 * if an array is given as first argument).
221 */
222 protected function setMwGlobals( $pairs, $value = null ) {
223 if ( !is_array( $pairs ) ) {
224 $key = $pairs;
225 $this->mwGlobals[$key] = $GLOBALS[$key];
226 $GLOBALS[$key] = $value;
227 } else {
228 foreach ( $pairs as $key => $value ) {
229 $this->mwGlobals[$key] = $GLOBALS[$key];
230 $GLOBALS[$key] = $value;
231 }
232 }
233 }
234
235 /**
236 * Merges the given values into a MW global array variable.
237 * Useful for setting some entries in a configuration array, instead of
238 * setting the entire array.
239 *
240 * @param String $name The name of the global, as in wgFooBar
241 * @param Array $values The array containing the entries to set in that global
242 *
243 * @throws MWException if the designated global is not an array.
244 */
245 protected function mergeMwGlobalArrayValue( $name, $values ) {
246 if ( !isset( $GLOBALS[$name] ) ) {
247 $merged = $values;
248 } else {
249 if ( !is_array( $GLOBALS[$name] ) ) {
250 throw new MWException( "MW global $name is not an array." );
251 }
252
253 // NOTE: do not use array_merge, it screws up for numeric keys.
254 $merged = $GLOBALS[$name];
255 foreach ( $values as $k => $v ) {
256 $merged[$k] = $v;
257 }
258 }
259
260 $this->setMwGlobals( $name, $merged );
261 }
262
263 function dbPrefix() {
264 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
265 }
266
267 function needsDB() {
268 # if the test says it uses database tables, it needs the database
269 if ( $this->tablesUsed ) {
270 return true;
271 }
272
273 # if the test says it belongs to the Database group, it needs the database
274 $rc = new ReflectionClass( $this );
275 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
276 return true;
277 }
278
279 return false;
280 }
281
282 /**
283 * Stub. If a test needs to add additional data to the database, it should
284 * implement this method and do so
285 */
286 function addDBData() {}
287
288 private function addCoreDBData() {
289 # disabled for performance
290 #$this->tablesUsed[] = 'page';
291 #$this->tablesUsed[] = 'revision';
292
293 if ( $this->db->getType() == 'oracle' ) {
294
295 # Insert 0 user to prevent FK violations
296 # Anonymous user
297 $this->db->insert( 'user', array(
298 'user_id' => 0,
299 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) );
300
301 # Insert 0 page to prevent FK violations
302 # Blank page
303 $this->db->insert( 'page', array(
304 'page_id' => 0,
305 'page_namespace' => 0,
306 'page_title' => ' ',
307 'page_restrictions' => NULL,
308 'page_counter' => 0,
309 'page_is_redirect' => 0,
310 'page_is_new' => 0,
311 'page_random' => 0,
312 'page_touched' => $this->db->timestamp(),
313 'page_latest' => 0,
314 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) );
315
316 }
317
318 User::resetIdByNameCache();
319
320 //Make sysop user
321 $user = User::newFromName( 'UTSysop' );
322
323 if ( $user->idForName() == 0 ) {
324 $user->addToDatabase();
325 $user->setPassword( 'UTSysopPassword' );
326
327 $user->addGroup( 'sysop' );
328 $user->addGroup( 'bureaucrat' );
329 $user->saveSettings();
330 }
331
332
333 //Make 1 page with 1 revision
334 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
335 if ( !$page->getId() == 0 ) {
336 $page->doEditContent(
337 new WikitextContent( 'UTContent' ),
338 'UTPageSummary',
339 EDIT_NEW,
340 false,
341 User::newFromName( 'UTSysop' ) );
342 }
343 }
344
345 private function initDB() {
346 global $wgDBprefix;
347 if ( $wgDBprefix === $this->dbPrefix() ) {
348 throw new MWException( 'Cannot run unit tests, the database prefix is already "unittest_"' );
349 }
350
351 $tablesCloned = $this->listTables();
352 $dbClone = new CloneDatabase( $this->db, $tablesCloned, $this->dbPrefix() );
353 $dbClone->useTemporaryTables( $this->useTemporaryTables );
354
355 if ( ( $this->db->getType() == 'oracle' || !$this->useTemporaryTables ) && $this->reuseDB ) {
356 CloneDatabase::changePrefix( $this->dbPrefix() );
357 $this->resetDB();
358 return;
359 } else {
360 $dbClone->cloneTableStructure();
361 }
362
363 if ( $this->db->getType() == 'oracle' ) {
364 $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
365 }
366 }
367
368 /**
369 * Empty all tables so they can be repopulated for tests
370 */
371 private function resetDB() {
372 if( $this->db ) {
373 if ( $this->db->getType() == 'oracle' ) {
374 if ( $this->useTemporaryTables ) {
375 wfGetLB()->closeAll();
376 $this->db = wfGetDB( DB_MASTER );
377 } else {
378 foreach( $this->tablesUsed as $tbl ) {
379 if( $tbl == 'interwiki') continue;
380 $this->db->query( 'TRUNCATE TABLE '.$this->db->tableName($tbl), __METHOD__ );
381 }
382 }
383 } else {
384 foreach( $this->tablesUsed as $tbl ) {
385 if( $tbl == 'interwiki' || $tbl == 'user' ) continue;
386 $this->db->delete( $tbl, '*', __METHOD__ );
387 }
388 }
389 }
390 }
391
392 function __call( $func, $args ) {
393 static $compatibility = array(
394 'assertInternalType' => 'assertType',
395 'assertNotInternalType' => 'assertNotType',
396 'assertInstanceOf' => 'assertType',
397 'assertEmpty' => 'assertEmpty2',
398 );
399
400 if ( method_exists( $this->suite, $func ) ) {
401 return call_user_func_array( array( $this->suite, $func ), $args);
402 } elseif ( isset( $compatibility[$func] ) ) {
403 return call_user_func_array( array( $this, $compatibility[$func] ), $args);
404 } else {
405 throw new MWException( "Called non-existant $func method on "
406 . get_class( $this ) );
407 }
408 }
409
410 private function assertEmpty2( $value, $msg ) {
411 return $this->assertTrue( $value == '', $msg );
412 }
413
414 static private function unprefixTable( $tableName ) {
415 global $wgDBprefix;
416 return substr( $tableName, strlen( $wgDBprefix ) );
417 }
418
419 static private function isNotUnittest( $table ) {
420 return strpos( $table, 'unittest_' ) !== 0;
421 }
422
423 protected function listTables() {
424 global $wgDBprefix;
425
426 $tables = $this->db->listTables( $wgDBprefix, __METHOD__ );
427 $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables );
428
429 // Don't duplicate test tables from the previous fataled run
430 $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) );
431
432 if ( $this->db->getType() == 'sqlite' ) {
433 $tables = array_flip( $tables );
434 // these are subtables of searchindex and don't need to be duped/dropped separately
435 unset( $tables['searchindex_content'] );
436 unset( $tables['searchindex_segdir'] );
437 unset( $tables['searchindex_segments'] );
438 $tables = array_flip( $tables );
439 }
440 return $tables;
441 }
442
443 protected function checkDbIsSupported() {
444 if( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
445 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
446 }
447 }
448
449 public function getCliArg( $offset ) {
450
451 if( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) {
452 return MediaWikiPHPUnitCommand::$additionalOptions[$offset];
453 }
454
455 }
456
457 public function setCliArg( $offset, $value ) {
458
459 MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value;
460
461 }
462
463 /**
464 * Don't throw a warning if $function is deprecated and called later
465 *
466 * @param $function String
467 * @return null
468 */
469 function hideDeprecated( $function ) {
470 wfSuppressWarnings();
471 wfDeprecated( $function );
472 wfRestoreWarnings();
473 }
474
475 /**
476 * Asserts that the given database query yields the rows given by $expectedRows.
477 * The expected rows should be given as indexed (not associative) arrays, with
478 * the values given in the order of the columns in the $fields parameter.
479 * Note that the rows are sorted by the columns given in $fields.
480 *
481 * @since 1.20
482 *
483 * @param $table String|Array the table(s) to query
484 * @param $fields String|Array the columns to include in the result (and to sort by)
485 * @param $condition String|Array "where" condition(s)
486 * @param $expectedRows Array - an array of arrays giving the expected rows.
487 *
488 * @throws MWException if this test cases's needsDB() method doesn't return true.
489 * Test cases can use "@group Database" to enable database test support,
490 * or list the tables under testing in $this->tablesUsed, or override the
491 * needsDB() method.
492 */
493 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
494 if ( !$this->needsDB() ) {
495 throw new MWException( 'When testing database state, the test cases\'s needDB()' .
496 ' method should return true. Use @group Database or $this->tablesUsed.');
497 }
498
499 $db = wfGetDB( DB_SLAVE );
500
501 $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) );
502 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
503
504 $i = 0;
505
506 foreach ( $expectedRows as $expected ) {
507 $r = $res->fetchRow();
508 self::stripStringKeys( $r );
509
510 $i += 1;
511 $this->assertNotEmpty( $r, "row #$i missing" );
512
513 $this->assertEquals( $expected, $r, "row #$i mismatches" );
514 }
515
516 $r = $res->fetchRow();
517 self::stripStringKeys( $r );
518
519 $this->assertFalse( $r, "found extra row (after #$i)" );
520 }
521
522 /**
523 * Utility method taking an array of elements and wrapping
524 * each element in it's own array. Useful for data providers
525 * that only return a single argument.
526 *
527 * @since 1.20
528 *
529 * @param array $elements
530 *
531 * @return array
532 */
533 protected function arrayWrap( array $elements ) {
534 return array_map(
535 function( $element ) {
536 return array( $element );
537 },
538 $elements
539 );
540 }
541
542 /**
543 * Assert that two arrays are equal. By default this means that both arrays need to hold
544 * the same set of values. Using additional arguments, order and associated key can also
545 * be set as relevant.
546 *
547 * @since 1.20
548 *
549 * @param array $expected
550 * @param array $actual
551 * @param boolean $ordered If the order of the values should match
552 * @param boolean $named If the keys should match
553 */
554 protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) {
555 if ( !$ordered ) {
556 $this->objectAssociativeSort( $expected );
557 $this->objectAssociativeSort( $actual );
558 }
559
560 if ( !$named ) {
561 $expected = array_values( $expected );
562 $actual = array_values( $actual );
563 }
564
565 call_user_func_array(
566 array( $this, 'assertEquals' ),
567 array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) )
568 );
569 }
570
571 /**
572 * Put each HTML element on its own line and then equals() the results
573 *
574 * Use for nicely formatting of PHPUnit diff output when comparing very
575 * simple HTML
576 *
577 * @since 1.20
578 *
579 * @param String $expected HTML on oneline
580 * @param String $actual HTML on oneline
581 * @param String $msg Optional message
582 */
583 protected function assertHTMLEquals( $expected, $actual, $msg='' ) {
584 $expected = str_replace( '>', ">\n", $expected );
585 $actual = str_replace( '>', ">\n", $actual );
586
587 $this->assertEquals( $expected, $actual, $msg );
588 }
589
590 /**
591 * Does an associative sort that works for objects.
592 *
593 * @since 1.20
594 *
595 * @param array $array
596 */
597 protected function objectAssociativeSort( array &$array ) {
598 uasort(
599 $array,
600 function( $a, $b ) {
601 return serialize( $a ) > serialize( $b ) ? 1 : -1;
602 }
603 );
604 }
605
606 /**
607 * Utility function for eliminating all string keys from an array.
608 * Useful to turn a database result row as returned by fetchRow() into
609 * a pure indexed array.
610 *
611 * @since 1.20
612 *
613 * @param $r mixed the array to remove string keys from.
614 */
615 protected static function stripStringKeys( &$r ) {
616 if ( !is_array( $r ) ) {
617 return;
618 }
619
620 foreach ( $r as $k => $v ) {
621 if ( is_string( $k ) ) {
622 unset( $r[$k] );
623 }
624 }
625 }
626
627 /**
628 * Asserts that the provided variable is of the specified
629 * internal type or equals the $value argument. This is useful
630 * for testing return types of functions that return a certain
631 * type or *value* when not set or on error.
632 *
633 * @since 1.20
634 *
635 * @param string $type
636 * @param mixed $actual
637 * @param mixed $value
638 * @param string $message
639 */
640 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
641 if ( $actual === $value ) {
642 $this->assertTrue( true, $message );
643 }
644 else {
645 $this->assertType( $type, $actual, $message );
646 }
647 }
648
649 /**
650 * Asserts the type of the provided value. This can be either
651 * in internal type such as boolean or integer, or a class or
652 * interface the value extends or implements.
653 *
654 * @since 1.20
655 *
656 * @param string $type
657 * @param mixed $actual
658 * @param string $message
659 */
660 protected function assertType( $type, $actual, $message = '' ) {
661 if ( class_exists( $type ) || interface_exists( $type ) ) {
662 $this->assertInstanceOf( $type, $actual, $message );
663 }
664 else {
665 $this->assertInternalType( $type, $actual, $message );
666 }
667 }
668
669 /**
670 * Returns true iff the given namespace defaults to Wikitext
671 * according to $wgNamespaceContentModels
672 *
673 * @param int $ns The namespace ID to check
674 *
675 * @return bool
676 * @since 1.21
677 */
678 protected function isWikitextNS( $ns ) {
679 global $wgNamespaceContentModels;
680
681 if ( isset( $wgNamespaceContentModels[$ns] ) ) {
682 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
683 }
684
685 return true;
686 }
687
688 /**
689 * Returns the ID of a namespace that defaults to Wikitext.
690 * Throws an MWException if there is none.
691 *
692 * @return int the ID of the wikitext Namespace
693 * @since 1.21
694 */
695 protected function getDefaultWikitextNS() {
696 global $wgNamespaceContentModels;
697
698 static $wikitextNS = null; // this is not going to change
699 if ( $wikitextNS !== null ) {
700 return $wikitextNS;
701 }
702
703 // quickly short out on most common case:
704 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
705 return NS_MAIN;
706 }
707
708 // NOTE: prefer content namespaces
709 $namespaces = array_unique( array_merge(
710 MWNamespace::getContentNamespaces(),
711 array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these
712 MWNamespace::getValidNamespaces()
713 ) );
714
715 $namespaces = array_diff( $namespaces, array(
716 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
717 ));
718
719 $talk = array_filter( $namespaces, function ( $ns ) {
720 return MWNamespace::isTalk( $ns );
721 } );
722
723 // prefer non-talk pages
724 $namespaces = array_diff( $namespaces, $talk );
725 $namespaces = array_merge( $namespaces, $talk );
726
727 // check default content model of each namespace
728 foreach ( $namespaces as $ns ) {
729 if ( !isset( $wgNamespaceContentModels[$ns] ) ||
730 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT ) {
731
732 $wikitextNS = $ns;
733 return $wikitextNS;
734 }
735 }
736
737 // give up
738 // @todo: Inside a test, we could skip the test as incomplete.
739 // But frequently, this is used in fixture setup.
740 throw new MWException( "No namespace defaults to wikitext!" );
741 }
742 }