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