use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
/**
* @ingroup Testing
*/
class ParserTestRunner {
+
+ /**
+ * MediaWiki core parser test files, paths
+ * will be prefixed with __DIR__ . '/'
+ *
+ * @var array
+ */
+ private static $coreTestFiles = [
+ 'parserTests.txt',
+ 'extraParserTests.txt',
+ ];
+
/**
* @var bool $useTemporaryTables Use temporary tables for the temporary database
*/
}
}
+ /**
+ * Get list of filenames to extension and core parser tests
+ *
+ * @return array
+ */
+ public static function getParserTestFiles() {
+ global $wgParserTestFiles;
+
+ // Add core test files
+ $files = array_map( function ( $item ) {
+ return __DIR__ . "/$item";
+ }, self::$coreTestFiles );
+
+ // Plus legacy global files
+ $files = array_merge( $files, $wgParserTestFiles );
+
+ // Auto-discover extension parser tests
+ $registry = ExtensionRegistry::getInstance();
+ foreach ( $registry->getAllThings() as $info ) {
+ $dir = dirname( $info['path'] ) . '/tests/parser';
+ if ( !file_exists( $dir ) ) {
+ continue;
+ }
+ $counter = 1;
+ $dirIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir )
+ );
+ foreach ( $dirIterator as $fileInfo ) {
+ /** @var SplFileInfo $fileInfo */
+ if ( substr( $fileInfo->getFilename(), -4 ) === '.txt' ) {
+ $name = $info['name'] . $counter;
+ while ( isset( $files[$name] ) ) {
+ $name = $info['name'] . '_' . $counter++;
+ }
+ $files[$name] = $fileInfo->getPathname();
+ }
+ }
+ }
+
+ return array_unique( $files );
+ }
+
public function getRecorder() {
return $this->recorder;
}
'name' => 'nullLockManager',
'class' => 'NullLockManager',
] ];
- $reset = function() {
+ $reset = function () {
LockManagerGroup::destroySingletons();
};
$setup[] = $reset;
$setup['wgSVGConverters'] = [ 'null' => 'echo "1">$output' ];
// Fake constant timestamp
- Hooks::register( 'ParserGetVariableValueTs', 'ParserTestRunner::getFakeTimestamp' );
+ Hooks::register( 'ParserGetVariableValueTs', function ( &$parser, &$ts ) {
+ $ts = $this->getFakeTimestamp();
+ return true;
+ } );
$teardown[] = function () {
Hooks::clear( 'ParserGetVariableValueTs' );
};
MediaWikiServices::getInstance()->disableService( 'MediaHandlerFactory' );
MediaWikiServices::getInstance()->redefineService(
'MediaHandlerFactory',
- function() {
- return new MockMediaHandlerFactory();
+ function ( MediaWikiServices $services ) {
+ $handlers = $services->getMainConfig()->get( 'ParserTestMediaHandlers' );
+ return new MediaHandlerFactory( $handlers );
}
);
$teardown[] = function () {
// Changing wgExtraNamespaces invalidates caches in MWNamespace and
// any live Language object, both on setup and teardown
$reset = function () {
- MWNamespace::getCanonicalNamespaces( true );
+ MWNamespace::clearCaches();
$GLOBALS['wgContLang']->resetNamespaces();
};
$setup[] = $reset;
* @see staticSetup
*
* @param array $teardown The snippet array
- * @param ScopedCallback|null A ScopedCallback to consume
+ * @param ScopedCallback|null $nextTeardown A ScopedCallback to consume
* @return ScopedCallback
*/
protected function createTeardownObject( $teardown, $nextTeardown = null ) {
- return new ScopedCallback( function() use ( $teardown, $nextTeardown ) {
+ return new ScopedCallback( function () use ( $teardown, $nextTeardown ) {
// Schedule teardown snippets in reverse order
$teardown = array_reverse( $teardown );
/**
* Ensure a given setup stage has been done, throw an exception if it has
* not.
+ * @param string $funcName
+ * @param string|null $funcName2
*/
protected function checkSetupDone( $funcName, $funcName2 = null ) {
if ( !$this->setupDone[$funcName]
* Determine whether a particular setup function has been run
*
* @param string $funcName
- * @return boolean
+ * @return bool
*/
public function isSetupDone( $funcName ) {
return isset( $this->setupDone[$funcName] ) ? $this->setupDone[$funcName] : false;
/**
* Determine whether the current parser has the hooks registered in it
* that are required by a file read by TestFileReader.
+ * @param array $requirements
+ * @return bool
*/
public function meetsRequirements( $requirements ) {
foreach ( $requirements as $requirement ) {
$context = RequestContext::getMain();
$user = $context->getUser();
$options = ParserOptions::newFromContext( $context );
+ $options->setTimestamp( $this->getFakeTimestamp() );
if ( !isset( $opts['wrap'] ) ) {
$options->setWrapOutputClass( false );
if ( isset( $opts['pst'] ) ) {
$out = $parser->preSaveTransform( $test['input'], $title, $user, $options );
+ $output = $parser->getOutput();
} elseif ( isset( $opts['msg'] ) ) {
$out = $parser->transformMsg( $test['input'], $options, $title );
} elseif ( isset( $opts['section'] ) ) {
}
}
+ if ( isset( $output ) && isset( $opts['showflags'] ) ) {
+ $actualFlags = array_keys( TestingAccessWrapper::newFromObject( $output )->mFlags );
+ sort( $actualFlags );
+ $out .= "\nflags=" . join( ', ', $actualFlags );
+ }
+
ScopedCallback::consume( $teardownGuard );
$expected = $test['result'];
$linkHolderBatchSize =
self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
+ // Default to fallback skin, but allow it to be overridden
+ $skin = self::getOptionValue( 'skin', $opts, 'fallback' );
+
$setup = [
'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
'wgLanguageCode' => $langCode,
// wgEnableMagicLinks={"ISBN":false, "PMID":false, "RFC":false}
'wgEnableMagicLinks' => self::getOptionValue( 'wgEnableMagicLinks', $opts, [] )
+ [ 'ISBN' => true, 'PMID' => true, 'RFC' => true ],
+ // Test with legacy encoding by default until HTML5 is very stable and default
+ 'wgFragmentMode' => [ 'legacy' ],
];
if ( $config ) {
$context = RequestContext::getMain();
$context->setUser( $user );
$context->setLanguage( $lang );
- $teardown[] = function () use ( $context ) {
+ // And the skin!
+ $oldSkin = $context->getSkin();
+ $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
+ $context->setSkin( $skinFactory->makeSkin( $skin ) );
+ $context->setOutput( new OutputPage( $context ) );
+ $setup['wgOut'] = $context->getOutput();
+ $teardown[] = function () use ( $context, $oldSkin ) {
+ // Clear language conversion tables
+ $wrapper = TestingAccessWrapper::newFromObject(
+ $context->getLanguage()->getConverter()
+ );
+ $wrapper->reloadTables();
// Reset context to the restored globals
$context->setUser( $GLOBALS['wgUser'] );
$context->setLanguage( $GLOBALS['wgContLang'] );
+ $context->setSkin( $oldSkin );
+ $context->setOutput( $GLOBALS['wgOut'] );
};
$teardown[] = $this->executeSetupSnippets( $setup );
*/
private function listTables() {
$tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
- 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
+ 'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
'site_stats', 'ipblocks', 'image', 'oldimage',
'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search',
/**
* Add articles to the test DB.
*
- * @param $articles Article info array from TestFileReader
+ * @param array $articles Article info array from TestFileReader
*/
public function addArticles( $articles ) {
global $wgContLang;
throw new MWException( "invalid title '$name' at $file:$line\n" );
}
+ $newContent = ContentHandler::makeContent( $text, $title );
+
$page = WikiPage::factory( $title );
$page->loadPageData( 'fromdbmaster' );
if ( $page->exists() ) {
- throw new MWException( "duplicate article '$name' at $file:$line\n" );
+ $content = $page->getContent( Revision::RAW );
+ // Only reject the title, if the content/content model is different.
+ // This makes it easier to create Template:(( or Template:)) in different extensions
+ if ( $newContent->equals( $content ) ) {
+ return;
+ }
+ throw new MWException(
+ "duplicate article '$name' with different content at $file:$line\n"
+ );
}
// Use mock parser, to make debugging of actual parser tests simpler.
// get a reference to the mock object.
MessageCache::singleton()->getParser();
$restore = $this->executeSetupSnippets( [ 'wgParser' => new ParserTestMockParser ] );
- $status = $page->doEditContent(
- ContentHandler::makeContent( $text, $title ),
- '',
- EDIT_NEW | EDIT_INTERNAL
- );
- $restore();
+ try {
+ $status = $page->doEditContent(
+ $newContent,
+ '',
+ EDIT_NEW | EDIT_INTERNAL
+ );
+ } finally {
+ $restore();
+ }
if ( !$status->isOK() ) {
throw new MWException( $status->getWikiText( false, false, 'en' ) );
}
/**
- * The ParserGetVariableValueTs hook, used to make sure time-related parser
+ * Fake constant timestamp to make sure time-related parser
* functions give a persistent value.
+ *
+ * - Parser::getVariableValue (via ParserGetVariableValueTs hook)
+ * - Parser::preSaveTransform (via ParserOptions)
*/
- static function getFakeTimestamp( &$parser, &$ts ) {
- $ts = 123; // parsed as '1970-01-01T00:02:03Z'
- return true;
+ private function getFakeTimestamp() {
+ // parsed as '1970-01-01T00:02:03Z'
+ return 123;
}
}