From: Tim Starling Date: Thu, 8 Sep 2016 01:07:06 +0000 (+1000) Subject: Renames preparatory to parser tests refactor X-Git-Tag: 1.31.0-rc.0~5656 X-Git-Url: http://git.cyclocoop.org/%24dirpuce/puce%24spip_lang_rtl.gif?a=commitdiff_plain;h=df29a359f85e31726e929ac22ebe6eb10b6ac9f9;p=lhc%2Fweb%2Fwiklou.git Renames preparatory to parser tests refactor Since in several cases, with an all-in-one commit, git's file rename detection failed, I split the renames out into their own commit to make review easier. Some changes here won't make complete sense without the following commit. * Moved TestsAutoLoader to tests/common/. It will be joined by a friend. * Renamed ParserTest to ParserTestRunner, since the former name was overly generic. * Renamed TestFileIterator to TestFileReader. Please see the subsequent commit for rationale. * Moved parserTests.php to tests/parser/. It was the only file left in tests/, and it should have been moved to tests/parser years ago, analogous to phpunit.php. * Renamed NewParserTest to ParserIntegrationTest. This was a tricky one, apparently the name has to end in "Test" or else the structure test will fail. Analogous to ParserMethodsTest etc. Rationale: because it's not new anymore. * Renamed MediaWikiParserTest to ParserTestTopLevelSuite and moved it to the suites directory. A more descriptive name. Being in suites/ shields it from StructureTests, and is correct anyway. Change-Id: Iddc6eaf815fdd64b3addb8570b4b6303ab99d634 --- diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index b53920b0fc..d83ea34efd 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -1775,7 +1775,7 @@ class Parser { * Replace external links (REL) * * Note: this is all very hackish and the order of execution matters a lot. - * Make sure to run tests/parserTests.php if you change this code. + * Make sure to run tests/parser/parserTests.php if you change this code. * * @private * diff --git a/maintenance/Makefile b/maintenance/Makefile index 2555475102..a348e856f3 100644 --- a/maintenance/Makefile +++ b/maintenance/Makefile @@ -4,7 +4,7 @@ help: @echo "Run 'make man' to run the doxygen generation with man pages." test: - php tests/parserTests.php --quiet + php tests/parser/parserTests.php --quiet doc: php mwdocgen.php --all diff --git a/maintenance/checkLess.php b/maintenance/checkLess.php index eeec9d1c89..df1868eb47 100644 --- a/maintenance/checkLess.php +++ b/maintenance/checkLess.php @@ -40,7 +40,7 @@ class CheckLess extends Maintenance { // NOTE (phuedx, 2014-03-26) wgAutoloadClasses isn't set up // by either of the dependencies at the top of the file, so // require it here. - require_once __DIR__ . '/../tests/TestsAutoLoader.php'; + require_once __DIR__ . '/../tests/common/TestsAutoLoader.php'; // If phpunit isn't available by autoloader try pulling it in if ( !class_exists( 'PHPUnit_Framework_TestCase' ) ) { diff --git a/tests/TestsAutoLoader.php b/tests/TestsAutoLoader.php deleted file mode 100644 index 4858703b92..0000000000 --- a/tests/TestsAutoLoader.php +++ /dev/null @@ -1,155 +0,0 @@ - "$testDir/phpunit/MediaWikiTestCase.php", - 'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php", - 'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php", - 'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'EmptyResourceLoader' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'TestUser' => "$testDir/phpunit/includes/TestUser.php", - 'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php", - 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php", - - # tests/phpunit/includes - 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php", - 'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php", - 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php", - - # tests/phpunit/includes/api - 'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php", - 'ApiQueryTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryTestBase.php", - 'ApiQueryContinueTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryContinueTestBase.php", - 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php", - 'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php", - 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php", - 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php", - 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php", - 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php", - 'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php", - - # tests/phpunit/includes/auth - 'MediaWiki\\Auth\\AuthenticationRequestTestCase' => - "$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php", - - # tests/phpunit/includes/changes - 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php", - - # tests/phpunit/includes/content - 'DummyContentHandlerForTesting' => - "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php", - 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php", - 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php", - 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php", - 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php", - 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php", - 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php", - 'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php", - - # tests/phpunit/includes/db - 'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php", - - # tests/phpunit/includes/diff - 'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php", - - # tests/phpunit/includes/logging - 'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php", - - # tests/phpunit/includes/page - 'WikiPageTest' => "$testDir/phpunit/includes/page/WikiPageTest.php", - - # tests/phpunit/includes/password - 'PasswordTestCase' => "$testDir/phpunit/includes/password/PasswordTestCase.php", - - # tests/phpunit/includes/resourceloader - 'ResourceLoaderImageModuleTest' => - "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", - 'ResourceLoaderImageModuleTestable' => - "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", - - # tests/phpunit/includes/session - 'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php", - 'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php", - - # tests/phpunit/includes/specials - 'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php", - 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", - - # tests/phpunit/languages - 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", - - # tests/phpunit/includes/libs - 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php", - - # tests/phpunit/maintenance - 'DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php", - - # tests/phpunit/media - 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php", - 'MediaWikiMediaTestCase' => "$testDir/phpunit/includes/media/MediaWikiMediaTestCase.php", - - # tests/phpunit/mocks - 'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php", - 'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php", - 'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php", - 'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php", - 'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php", - 'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php", - 'MockOggHandler' => "$testDir/phpunit/mocks/media/MockOggHandler.php", - 'MockMediaHandlerFactory' => "$testDir/phpunit/mocks/media/MockMediaHandlerFactory.php", - 'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php", - 'MediaWiki\\Session\\DummySessionBackend' - => "$testDir/phpunit/mocks/session/DummySessionBackend.php", - 'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php", - - # tests/parser - 'DbTestPreviewer' => "$testDir/parser/DbTestPreviewer.php", - 'DbTestRecorder' => "$testDir/parser/DbTestRecorder.php", - 'DelayedParserTest' => "$testDir/parser/DelayedParserTest.php", - 'DjVuSupport' => "$testDir/parser/DjVuSupport.php", - 'ITestRecorder' => "$testDir/parser/ITestRecorder.php", - 'MediaWikiParserTest' => "$testDir/phpunit/includes/parser/MediaWikiParserTest.php", - 'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php", - 'ParserTest' => "$testDir/parser/ParserTest.php", - 'ParserTestParserHook' => "$testDir/parser/ParserTestParserHook.php", - 'ParserTestResult' => "$testDir/parser/ParserTestResult.php", - 'ParserTestResultNormalizer' => "$testDir/parser/ParserTestResultNormalizer.php", - 'TestFileDataProvider' => "$testDir/parser/TestFileDataProvider.php", - 'TestFileIterator' => "$testDir/parser/TestFileIterator.php", - 'TestRecorder' => "$testDir/parser/TestRecorder.php", - 'TidySupport' => "$testDir/parser/TidySupport.php", - - # tests/phpunit/includes/site - 'SiteTest' => "$testDir/phpunit/includes/site/SiteTest.php", - 'TestSites' => "$testDir/phpunit/includes/site/TestSites.php", - - # tests/phpunit/includes/specialpage - 'SpecialPageTestHelper' => "$testDir/phpunit/includes/specialpage/SpecialPageTestHelper.php", -]; diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php new file mode 100644 index 0000000000..7de339472d --- /dev/null +++ b/tests/common/TestsAutoLoader.php @@ -0,0 +1,157 @@ + "$testDir/phpunit/MediaWikiTestCase.php", + 'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php", + 'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php", + 'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php", + 'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", + 'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", + 'EmptyResourceLoader' => "$testDir/phpunit/ResourceLoaderTestCase.php", + 'TestUser' => "$testDir/phpunit/includes/TestUser.php", + 'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php", + 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php", + + # tests/phpunit/includes + 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php", + 'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php", + 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php", + + # tests/phpunit/includes/api + 'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php", + 'ApiQueryTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryTestBase.php", + 'ApiQueryContinueTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryContinueTestBase.php", + 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php", + 'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php", + 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php", + 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php", + 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php", + 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php", + 'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php", + + # tests/phpunit/includes/auth + 'MediaWiki\\Auth\\AuthenticationRequestTestCase' => + "$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php", + + # tests/phpunit/includes/changes + 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php", + + # tests/phpunit/includes/content + 'DummyContentHandlerForTesting' => + "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php", + 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php", + 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php", + 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php", + 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php", + 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php", + 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php", + 'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php", + + # tests/phpunit/includes/db + 'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php", + + # tests/phpunit/includes/diff + 'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php", + + # tests/phpunit/includes/logging + 'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php", + + # tests/phpunit/includes/page + 'WikiPageTest' => "$testDir/phpunit/includes/page/WikiPageTest.php", + + # tests/phpunit/includes/password + 'PasswordTestCase' => "$testDir/phpunit/includes/password/PasswordTestCase.php", + + # tests/phpunit/includes/resourceloader + 'ResourceLoaderImageModuleTest' => + "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", + 'ResourceLoaderImageModuleTestable' => + "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", + + # tests/phpunit/includes/session + 'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php", + 'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php", + + # tests/phpunit/includes/specials + 'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php", + 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", + + # tests/phpunit/languages + 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", + + # tests/phpunit/includes/libs + 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php", + + # tests/phpunit/maintenance + 'DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php", + + # tests/phpunit/media + 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php", + 'MediaWikiMediaTestCase' => "$testDir/phpunit/includes/media/MediaWikiMediaTestCase.php", + + # tests/phpunit/mocks + 'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php", + 'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php", + 'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php", + 'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php", + 'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php", + 'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php", + 'MockOggHandler' => "$testDir/phpunit/mocks/media/MockOggHandler.php", + 'MockMediaHandlerFactory' => "$testDir/phpunit/mocks/media/MockMediaHandlerFactory.php", + 'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php", + 'MediaWiki\\Session\\DummySessionBackend' + => "$testDir/phpunit/mocks/session/DummySessionBackend.php", + 'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php", + + # tests/parser + 'DbTestPreviewer' => "$testDir/parser/DbTestPreviewer.php", + 'DbTestRecorder' => "$testDir/parser/DbTestRecorder.php", + 'DelayedParserTest' => "$testDir/parser/DelayedParserTest.php", + 'DjVuSupport' => "$testDir/parser/DjVuSupport.php", + 'ITestRecorder' => "$testDir/parser/ITestRecorder.php", + 'ParserIntegrationTest' => "$testDir/phpunit/includes/parser/ParserIntegrationTest.php", + 'ParserTestRunner' => "$testDir/parser/ParserTestRunner.php", + 'ParserTestParserHook' => "$testDir/parser/ParserTestParserHook.php", + 'ParserTestResult' => "$testDir/parser/ParserTestResult.php", + 'ParserTestResultNormalizer' => "$testDir/parser/ParserTestResultNormalizer.php", + 'TestFileDataProvider' => "$testDir/parser/TestFileDataProvider.php", + 'TestFileReader' => "$testDir/parser/TestFileReader.php", + 'TestRecorder' => "$testDir/parser/TestRecorder.php", + 'TidySupport' => "$testDir/parser/TidySupport.php", + + # tests/phpunit/includes/site + 'SiteTest' => "$testDir/phpunit/includes/site/SiteTest.php", + 'TestSites' => "$testDir/phpunit/includes/site/TestSites.php", + + # tests/phpunit/includes/specialpage + 'SpecialPageTestHelper' => "$testDir/phpunit/includes/specialpage/SpecialPageTestHelper.php", + + # tests/phpunit/suites + 'ParserTestTopLevelSuite' => "$testDir/phpunit/suites/ParserTestTopLevelSuite.php", +]; diff --git a/tests/parser/DelayedParserTest.php b/tests/parser/DelayedParserTest.php index 1c5c36bf0e..f9ece9295b 100644 --- a/tests/parser/DelayedParserTest.php +++ b/tests/parser/DelayedParserTest.php @@ -47,14 +47,16 @@ class DelayedParserTest { /** * Called whenever we actually want to run the hook. * Should be the case if we found the parserTest is not disabled - * @param ParserTest|NewParserTest $parserTest + * @param ParserTestRunner|ParserIntegrationTest $parserTest * @return bool * @throws MWException */ public function unleash( &$parserTest ) { - if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) { - throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or " - . "NewParserTest classes\n" ); + if ( !( $parserTest instanceof ParserTestRunner + || $parserTest instanceof ParserIntegrationTest ) + ) { + throw new MWException( __METHOD__ . " must be passed an instance of " . + "ParserTestRunner or ParserIntegrationTest classes\n" ); } # Trigger delayed hooks. Any failure will make us abort @@ -86,7 +88,7 @@ class DelayedParserTest { } /** - * Similar to ParserTest object but does not run anything + * Similar to ParserTestRunner object but does not run anything * Use unleash() to really execute the hook * @param string $hook */ @@ -95,7 +97,7 @@ class DelayedParserTest { } /** - * Similar to ParserTest object but does not run anything + * Similar to ParserTestRunner object but does not run anything * Use unleash() to really execute the hook function * @param string $fnHook */ @@ -104,7 +106,7 @@ class DelayedParserTest { } /** - * Similar to ParserTest object but does not run anything + * Similar to ParserTestRunner object but does not run anything * Use unleash() to really execute the hook function * @param string $hook */ diff --git a/tests/parser/ParserTest.php b/tests/parser/ParserTest.php deleted file mode 100644 index 7b3746abe0..0000000000 --- a/tests/parser/ParserTest.php +++ /dev/null @@ -1,1591 +0,0 @@ - - * https://www.mediawiki.org/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @todo Make this more independent of the configuration (and if possible the database) - * @todo document - * @file - * @ingroup Testing - */ -use MediaWiki\MediaWikiServices; - -/** - * @ingroup Testing - */ -class ParserTest { - /** - * @var bool $color whereas output should be colorized - */ - private $color; - - /** - * @var bool $showOutput Show test output - */ - private $showOutput; - - /** - * @var bool $useTemporaryTables Use temporary tables for the temporary database - */ - private $useTemporaryTables = true; - - /** - * @var bool $databaseSetupDone True if the database has been set up - */ - private $databaseSetupDone = false; - - /** - * Our connection to the database - * @var DatabaseBase - */ - private $db; - - /** - * Database clone helper - * @var CloneDatabase - */ - private $dbClone; - - /** - * @var DjVuSupport - */ - private $djVuSupport; - - /** - * @var TidySupport - */ - private $tidySupport; - - /** - * @var ITestRecorder - */ - private $recorder; - - private $uploadDir = null; - - public $regex = ""; - private $savedGlobals = []; - private $useDwdiff = false; - private $markWhitespace = false; - private $normalizationFunctions = []; - - /** - * Sets terminal colorization and diff/quick modes depending on OS and - * command-line options (--color and --quick). - * @param array $options - */ - public function __construct( $options = [] ) { - # Only colorize output if stdout is a terminal. - $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 ); - - if ( isset( $options['color'] ) ) { - switch ( $options['color'] ) { - case 'no': - $this->color = false; - break; - case 'yes': - default: - $this->color = true; - break; - } - } - - $this->term = $this->color - ? new AnsiTermColorer() - : new DummyTermColorer(); - - $this->showDiffs = !isset( $options['quick'] ); - $this->showProgress = !isset( $options['quiet'] ); - $this->showFailure = !( - isset( $options['quiet'] ) - && ( isset( $options['record'] ) - || isset( $options['compare'] ) ) ); // redundant output - - $this->showOutput = isset( $options['show-output'] ); - $this->useDwdiff = isset( $options['dwdiff'] ); - $this->markWhitespace = isset( $options['mark-ws'] ); - - if ( isset( $options['norm'] ) ) { - foreach ( explode( ',', $options['norm'] ) as $func ) { - if ( in_array( $func, [ 'removeTbody', 'trimWhitespace' ] ) ) { - $this->normalizationFunctions[] = $func; - } else { - echo "Warning: unknown normalization option \"$func\"\n"; - } - } - } - - if ( isset( $options['filter'] ) ) { - $options['regex'] = $options['filter']; - } - - if ( isset( $options['regex'] ) ) { - if ( isset( $options['record'] ) ) { - echo "Warning: --record cannot be used with --regex, disabling --record\n"; - unset( $options['record'] ); - } - $this->regex = $options['regex']; - } else { - # Matches anything - $this->regex = ''; - } - - $this->setupRecorder( $options ); - $this->keepUploads = isset( $options['keep-uploads'] ); - - if ( $this->keepUploads ) { - $this->uploadDir = wfTempDir() . '/mwParser-images'; - } else { - $this->uploadDir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; - } - - $this->runDisabled = isset( $options['run-disabled'] ); - $this->runParsoid = isset( $options['run-parsoid'] ); - - $this->djVuSupport = new DjVuSupport(); - $this->tidySupport = new TidySupport( isset( $options['use-tidy-config'] ) ); - if ( !$this->tidySupport->isEnabled() ) { - echo "Warning: tidy is not installed, skipping some tests\n"; - } - - $this->hooks = []; - $this->functionHooks = []; - $this->transparentHooks = []; - $this->setUp(); - } - - function setUp() { - global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, - $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, - $wgExtraNamespaces, $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, - $wgExtraInterlanguageLinkPrefixes, $wgLocalInterwikis, - $parserMemc, $wgThumbnailScriptPath, $wgScriptPath, $wgResourceBasePath, - $wgArticlePath, $wgScript, $wgStylePath, $wgExtensionAssetsPath, - $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers; - - $wgScriptPath = ''; - $wgScript = '/index.php'; - $wgStylePath = '/skins'; - $wgResourceBasePath = ''; - $wgExtensionAssetsPath = '/extensions'; - $wgArticlePath = '/wiki/$1'; - $wgThumbnailScriptPath = false; - $wgLockManagers = [ [ - 'name' => 'fsLockManager', - 'class' => 'FSLockManager', - 'lockDirectory' => $this->uploadDir . '/lockdir', - ], [ - 'name' => 'nullLockManager', - 'class' => 'NullLockManager', - ] ]; - $wgLocalFileRepo = [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => new FSFileBackend( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID(), - 'containerPaths' => [ - 'local-public' => $this->uploadDir . '/public', - 'local-thumb' => $this->uploadDir . '/thumb', - 'local-temp' => $this->uploadDir . '/temp', - 'local-deleted' => $this->uploadDir . '/deleted', - ] - ] ) - ]; - $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; - $wgNamespaceAliases['Image'] = NS_FILE; - $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; - # add a namespace shadowing a interwiki link, to test - # proper precedence when resolving links. (bug 51680) - $wgExtraNamespaces[100] = 'MemoryAlpha'; - $wgExtraNamespaces[101] = 'MemoryAlpha talk'; - - // XXX: tests won't run without this (for CACHE_DB) - if ( $wgMainCacheType === CACHE_DB ) { - $wgMainCacheType = CACHE_NONE; - } - if ( $wgMessageCacheType === CACHE_DB ) { - $wgMessageCacheType = CACHE_NONE; - } - if ( $wgParserCacheType === CACHE_DB ) { - $wgParserCacheType = CACHE_NONE; - } - - DeferredUpdates::clearPendingUpdates(); - $wgMemc = wfGetMainCache(); // checks $wgMainCacheType - $messageMemc = wfGetMessageCacheStorage(); - $parserMemc = wfGetParserCacheStorage(); - - RequestContext::resetMain(); - $context = new RequestContext; - $wgUser = new User; - $wgLang = $context->getLanguage(); - $wgOut = $context->getOutput(); - $wgRequest = $context->getRequest(); - $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], [ $wgParserConf ] ); - - if ( $wgStyleDirectory === false ) { - $wgStyleDirectory = "$IP/skins"; - } - - self::setupInterwikis(); - $wgLocalInterwikis = [ 'local', 'mi' ]; - // "extra language links" - // see https://gerrit.wikimedia.org/r/111390 - array_push( $wgExtraInterlanguageLinkPrefixes, 'mul' ); - - // Reset namespace cache - MWNamespace::getCanonicalNamespaces( true ); - Language::factory( 'en' )->resetNamespaces(); - } - - /** - * Insert hardcoded interwiki in the lookup table. - * - * This function insert a set of well known interwikis that are used in - * the parser tests. They can be considered has fixtures are injected in - * the interwiki cache by using the 'InterwikiLoadPrefix' hook. - * Since we are not interested in looking up interwikis in the database, - * the hook completely replace the existing mechanism (hook returns false). - */ - public static function setupInterwikis() { - # Hack: insert a few Wikipedia in-project interwiki prefixes, - # for testing inter-language links - Hooks::register( 'InterwikiLoadPrefix', function ( $prefix, &$iwData ) { - static $testInterwikis = [ - 'local' => [ - 'iw_url' => 'http://doesnt.matter.org/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'wikipedia' => [ - 'iw_url' => 'http://en.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'meatball' => [ - 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'memoryalpha' => [ - 'iw_url' => 'http://www.memory-alpha.org/en/index.php/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'zh' => [ - 'iw_url' => 'http://zh.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'es' => [ - 'iw_url' => 'http://es.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'fr' => [ - 'iw_url' => 'http://fr.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'ru' => [ - 'iw_url' => 'http://ru.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'mi' => [ - 'iw_url' => 'http://mi.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'mul' => [ - 'iw_url' => 'http://wikisource.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - ]; - if ( array_key_exists( $prefix, $testInterwikis ) ) { - $iwData = $testInterwikis[$prefix]; - } - - // We only want to rely on the above fixtures - return false; - } );// hooks::register - } - - /** - * Remove the hardcoded interwiki lookup table. - */ - public static function tearDownInterwikis() { - Hooks::clear( 'InterwikiLoadPrefix' ); - } - - /** - * Reset the Title-related services that need resetting - * for each test - */ - public static function resetTitleServices() { - $services = MediaWikiServices::getInstance(); - $services->resetServiceForTesting( 'TitleFormatter' ); - $services->resetServiceForTesting( 'TitleParser' ); - $services->resetServiceForTesting( '_MediaWikiTitleCodec' ); - $services->resetServiceForTesting( 'LinkRenderer' ); - $services->resetServiceForTesting( 'LinkRendererFactory' ); - } - - public function setupRecorder( $options ) { - if ( isset( $options['record'] ) ) { - $this->recorder = new DbTestRecorder( $this ); - $this->recorder->version = isset( $options['setversion'] ) ? - $options['setversion'] : SpecialVersion::getVersion(); - } elseif ( isset( $options['compare'] ) ) { - $this->recorder = new DbTestPreviewer( $this ); - } else { - $this->recorder = new TestRecorder( $this ); - } - } - - /** - * Remove last character if it is a newline - * @group utility - * @param string $s - * @return string - */ - public static function chomp( $s ) { - if ( substr( $s, -1 ) === "\n" ) { - return substr( $s, 0, -1 ); - } else { - return $s; - } - } - - /** - * Run a series of tests listed in the given text files. - * Each test consists of a brief description, wikitext input, - * and the expected HTML output. - * - * Prints status updates on stdout and counts up the total - * number and percentage of passed tests. - * - * @param array $filenames Array of strings - * @return bool True if passed all tests, false if any tests failed. - */ - public function runTestsFromFiles( $filenames ) { - $ok = false; - - // be sure, ParserTest::addArticle has correct language set, - // so that system messages gets into the right language cache - $GLOBALS['wgLanguageCode'] = 'en'; - $GLOBALS['wgContLang'] = Language::factory( 'en' ); - - $this->recorder->start(); - try { - $this->setupDatabase(); - $ok = true; - - foreach ( $filenames as $filename ) { - echo "Running parser tests from: $filename\n"; - $tests = new TestFileIterator( $filename, $this ); - $ok = $this->runTests( $tests ) && $ok; - } - - $this->teardownDatabase(); - $this->recorder->report(); - } catch ( DBError $e ) { - echo $e->getMessage(); - } - $this->recorder->end(); - - return $ok; - } - - function runTests( $tests ) { - $ok = true; - - foreach ( $tests as $t ) { - $result = - $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] ); - $ok = $ok && $result; - $this->recorder->record( $t['test'], $t['subtest'], $result ); - } - - if ( $this->showProgress ) { - print "\n"; - } - - return $ok; - } - - /** - * Get a Parser object - * - * @param string $preprocessor - * @return Parser - */ - function getParser( $preprocessor = null ) { - global $wgParserConf; - - $class = $wgParserConf['class']; - $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); - - foreach ( $this->hooks as $tag => $callback ) { - $parser->setHook( $tag, $callback ); - } - - foreach ( $this->functionHooks as $tag => $bits ) { - list( $callback, $flags ) = $bits; - $parser->setFunctionHook( $tag, $callback, $flags ); - } - - foreach ( $this->transparentHooks as $tag => $callback ) { - $parser->setTransparentTagHook( $tag, $callback ); - } - - Hooks::run( 'ParserTestParser', [ &$parser ] ); - - return $parser; - } - - /** - * Run a given wikitext input through a freshly-constructed wiki parser, - * and compare the output against the expected results. - * Prints status and explanatory messages to stdout. - * - * @param string $desc Test's description - * @param string $input Wikitext to try rendering - * @param string $result Result to output - * @param array $opts Test's options - * @param string $config Overrides for global variables, one per line - * @return bool - */ - public function runTest( $desc, $input, $result, $opts, $config ) { - if ( $this->showProgress ) { - $this->showTesting( $desc ); - } - - $opts = $this->parseOptions( $opts ); - $context = $this->setupGlobals( $opts, $config ); - - $user = $context->getUser(); - $options = ParserOptions::newFromContext( $context ); - - if ( isset( $opts['djvu'] ) ) { - if ( !$this->djVuSupport->isEnabled() ) { - return $this->showSkipped(); - } - } - - if ( isset( $opts['tidy'] ) ) { - if ( !$this->tidySupport->isEnabled() ) { - return $this->showSkipped(); - } else { - $options->setTidy( true ); - } - } - - if ( isset( $opts['title'] ) ) { - $titleText = $opts['title']; - } else { - $titleText = 'Parser test'; - } - - ObjectCache::getMainWANInstance()->clearProcessCache(); - $local = isset( $opts['local'] ); - $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; - $parser = $this->getParser( $preprocessor ); - $title = Title::newFromText( $titleText ); - - if ( isset( $opts['pst'] ) ) { - $out = $parser->preSaveTransform( $input, $title, $user, $options ); - } elseif ( isset( $opts['msg'] ) ) { - $out = $parser->transformMsg( $input, $options, $title ); - } elseif ( isset( $opts['section'] ) ) { - $section = $opts['section']; - $out = $parser->getSection( $input, $section ); - } elseif ( isset( $opts['replace'] ) ) { - $section = $opts['replace'][0]; - $replace = $opts['replace'][1]; - $out = $parser->replaceSection( $input, $section, $replace ); - } elseif ( isset( $opts['comment'] ) ) { - $out = Linker::formatComment( $input, $title, $local ); - } elseif ( isset( $opts['preload'] ) ) { - $out = $parser->getPreloadText( $input, $title, $options ); - } else { - $output = $parser->parse( $input, $title, $options, true, true, 1337 ); - $output->setTOCEnabled( !isset( $opts['notoc'] ) ); - $out = $output->getText(); - if ( isset( $opts['tidy'] ) ) { - $out = preg_replace( '/\s+$/', '', $out ); - } - - if ( isset( $opts['showtitle'] ) ) { - if ( $output->getTitleText() ) { - $title = $output->getTitleText(); - } - - $out = "$title\n$out"; - } - - if ( isset( $opts['showindicators'] ) ) { - $indicators = ''; - foreach ( $output->getIndicators() as $id => $content ) { - $indicators .= "$id=$content\n"; - } - $out = $indicators . $out; - } - - if ( isset( $opts['ill'] ) ) { - $out = implode( ' ', $output->getLanguageLinks() ); - } elseif ( isset( $opts['cat'] ) ) { - $outputPage = $context->getOutput(); - $outputPage->addCategoryLinks( $output->getCategories() ); - $cats = $outputPage->getCategoryLinks(); - - if ( isset( $cats['normal'] ) ) { - $out = implode( ' ', $cats['normal'] ); - } else { - $out = ''; - } - } - } - - $this->teardownGlobals(); - - if ( count( $this->normalizationFunctions ) ) { - $result = ParserTestResultNormalizer::normalize( $result, $this->normalizationFunctions ); - $out = ParserTestResultNormalizer::normalize( $out, $this->normalizationFunctions ); - } - - $testResult = new ParserTestResult( $desc ); - $testResult->expected = $result; - $testResult->actual = $out; - - return $this->showTestResult( $testResult ); - } - - /** - * Refactored in 1.22 to use ParserTestResult - * @param ParserTestResult $testResult - * @return bool - */ - function showTestResult( ParserTestResult $testResult ) { - if ( $testResult->isSuccess() ) { - $this->showSuccess( $testResult ); - return true; - } else { - $this->showFailure( $testResult ); - return false; - } - } - - /** - * Use a regex to find out the value of an option - * @param string $key Name of option val to retrieve - * @param array $opts Options array to look in - * @param mixed $default Default value returned if not found - * @return mixed - */ - private static function getOptionValue( $key, $opts, $default ) { - $key = strtolower( $key ); - - if ( isset( $opts[$key] ) ) { - return $opts[$key]; - } else { - return $default; - } - } - - private function parseOptions( $instring ) { - $opts = []; - // foo - // foo=bar - // foo="bar baz" - // foo=[[bar baz]] - // foo=bar,"baz quux" - // foo={...json...} - $defs = '(?(DEFINE) - (? # Quoted string - " - (?:[^\\\\"] | \\\\.)* - " - ) - (? - \{ # Open bracket - (?: - [^"{}] | # Not a quoted string or object, or - (?&qstr) | # A quoted string, or - (?&json) # A json object (recursively) - )* - \} # Close bracket - ) - (? - (?: - (?&qstr) # Quoted val - | - \[\[ - [^]]* # Link target - \]\] - | - [\w-]+ # Plain word - | - (?&json) # JSON object - ) - ) - )'; - $regex = '/' . $defs . '\b - (?[\w-]+) # Key - \b - (?:\s* - = # First sub-value - \s* - (? - (?&value) - (?:\s* - , # Sub-vals 1..N - \s* - (?&value) - )* - ) - )? - /x'; - $valueregex = '/' . $defs . '(?&value)/x'; - - if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { - foreach ( $matches as $bits ) { - $key = strtolower( $bits['k'] ); - if ( !isset( $bits['v'] ) ) { - $opts[$key] = true; - } else { - preg_match_all( $valueregex, $bits['v'], $vmatches ); - $opts[$key] = array_map( [ $this, 'cleanupOption' ], $vmatches[0] ); - if ( count( $opts[$key] ) == 1 ) { - $opts[$key] = $opts[$key][0]; - } - } - } - } - return $opts; - } - - private function cleanupOption( $opt ) { - if ( substr( $opt, 0, 1 ) == '"' ) { - return stripcslashes( substr( $opt, 1, -1 ) ); - } - - if ( substr( $opt, 0, 2 ) == '[[' ) { - return substr( $opt, 2, -2 ); - } - - if ( substr( $opt, 0, 1 ) == '{' ) { - return FormatJson::decode( $opt, true ); - } - return $opt; - } - - /** - * Set up the global variables for a consistent environment for each test. - * Ideally this should replace the global configuration entirely. - * @param string $opts - * @param string $config - * @return RequestContext - */ - public function setupGlobals( $opts = '', $config = '' ) { - # Find out values for some special options. - $lang = - self::getOptionValue( 'language', $opts, 'en' ); - $variant = - self::getOptionValue( 'variant', $opts, false ); - $maxtoclevel = - self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); - $linkHolderBatchSize = - self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); - - $settings = [ - 'wgServer' => 'http://example.org', - 'wgServerName' => 'example.org', - 'wgScript' => '/index.php', - 'wgScriptPath' => '', - 'wgArticlePath' => '/wiki/$1', - 'wgActionPaths' => [], - 'wgLockManagers' => [ [ - 'name' => 'fsLockManager', - 'class' => 'FSLockManager', - 'lockDirectory' => $this->uploadDir . '/lockdir', - ], [ - 'name' => 'nullLockManager', - 'class' => 'NullLockManager', - ] ], - 'wgLocalFileRepo' => [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => new FSFileBackend( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID(), - 'containerPaths' => [ - 'local-public' => $this->uploadDir, - 'local-thumb' => $this->uploadDir . '/thumb', - 'local-temp' => $this->uploadDir . '/temp', - 'local-deleted' => $this->uploadDir . '/delete', - ] - ] ) - ], - 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), - 'wgUploadNavigationUrl' => false, - 'wgStylePath' => '/skins', - 'wgSitename' => 'MediaWiki', - 'wgLanguageCode' => $lang, - 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_', - 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), - 'wgLang' => null, - 'wgContLang' => null, - 'wgNamespacesWithSubpages' => [ 0 => isset( $opts['subpage'] ) ], - 'wgMaxTocLevel' => $maxtoclevel, - 'wgCapitalLinks' => true, - 'wgNoFollowLinks' => true, - 'wgNoFollowDomainExceptions' => [ 'no-nofollow.org' ], - 'wgThumbnailScriptPath' => false, - 'wgUseImageResize' => true, - 'wgSVGConverter' => 'null', - 'wgSVGConverters' => [ 'null' => 'echo "1">$output' ], - 'wgLocaltimezone' => 'UTC', - 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), - 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], - 'wgDefaultLanguageVariant' => $variant, - 'wgVariantArticlePath' => false, - 'wgGroupPermissions' => [ '*' => [ - 'createaccount' => true, - 'read' => true, - 'edit' => true, - 'createpage' => true, - 'createtalk' => true, - ] ], - 'wgNamespaceProtection' => [ NS_MEDIAWIKI => 'editinterface' ], - 'wgDefaultExternalStore' => [], - 'wgForeignFileRepos' => [], - 'wgLinkHolderBatchSize' => $linkHolderBatchSize, - 'wgExperimentalHtmlIds' => false, - 'wgExternalLinkTarget' => false, - 'wgHtml5' => true, - 'wgAdaptiveMessageCache' => true, - 'wgDisableLangConversion' => false, - 'wgDisableTitleConversion' => false, - // Tidy options. - 'wgUseTidy' => false, - 'wgTidyConfig' => isset( $opts['tidy'] ) ? $this->tidySupport->getConfig() : null - ]; - - if ( $config ) { - $configLines = explode( "\n", $config ); - - foreach ( $configLines as $line ) { - list( $var, $value ) = explode( '=', $line, 2 ); - - $settings[$var] = eval( "return $value;" ); - } - } - - $this->savedGlobals = []; - - /** @since 1.20 */ - Hooks::run( 'ParserTestGlobals', [ &$settings ] ); - - foreach ( $settings as $var => $val ) { - if ( array_key_exists( $var, $GLOBALS ) ) { - $this->savedGlobals[$var] = $GLOBALS[$var]; - } - - $GLOBALS[$var] = $val; - } - - // Must be set before $context as user language defaults to $wgContLang - $GLOBALS['wgContLang'] = Language::factory( $lang ); - $GLOBALS['wgMemc'] = new EmptyBagOStuff; - - RequestContext::resetMain(); - $context = RequestContext::getMain(); - $GLOBALS['wgLang'] = $context->getLanguage(); - $GLOBALS['wgOut'] = $context->getOutput(); - $GLOBALS['wgUser'] = $context->getUser(); - - // We (re)set $wgThumbLimits to a single-element array above. - $context->getUser()->setOption( 'thumbsize', 0 ); - - global $wgHooks; - - $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; - $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; - - MagicWord::clearCache(); - MWTidy::destroySingleton(); - RepoGroup::destroySingleton(); - - self::resetTitleServices(); - - return $context; - } - - /** - * List of temporary tables to create, without prefix. - * Some of these probably aren't necessary. - * @return array - */ - private function listTables() { - $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions', - 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks', - 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks', - 'site_stats', 'ipblocks', 'image', 'oldimage', - 'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search', - 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo', - 'archive', 'user_groups', 'page_props', 'category' - ]; - - if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) { - array_push( $tables, 'searchindex' ); - } - - // Allow extensions to add to the list of tables to duplicate; - // may be necessary if they hook into page save or other code - // which will require them while running tests. - Hooks::run( 'ParserTestTables', [ &$tables ] ); - - return $tables; - } - - /** - * Set up a temporary set of wiki tables to work with for the tests. - * Currently this will only be done once per run, and any changes to - * the db will be visible to later tests in the run. - */ - public function setupDatabase() { - global $wgDBprefix; - - if ( $this->databaseSetupDone ) { - return; - } - - $this->db = wfGetDB( DB_MASTER ); - $dbType = $this->db->getType(); - - if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) { - throw new MWException( 'setupDatabase should be called before setupGlobals' ); - } - - $this->databaseSetupDone = true; - - # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892). - # It seems to have been fixed since (r55079?), but regressed at some point before r85701. - # This works around it for now... - ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; - - # CREATE TEMPORARY TABLE breaks if there is more than one server - if ( wfGetLB()->getServerCount() != 1 ) { - $this->useTemporaryTables = false; - } - - $temporary = $this->useTemporaryTables || $dbType == 'postgres'; - $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_'; - - $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix ); - $this->dbClone->useTemporaryTables( $temporary ); - $this->dbClone->cloneTableStructure(); - - if ( $dbType == 'oracle' ) { - $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); - # Insert 0 user to prevent FK violations - - # Anonymous user - $this->db->insert( 'user', [ - 'user_id' => 0, - 'user_name' => 'Anonymous' ] ); - } - - # Update certain things in site_stats - $this->db->insert( 'site_stats', - [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ] ); - - # Reinitialise the LocalisationCache to match the database state - Language::getLocalisationCache()->unloadAll(); - - # Clear the message cache - MessageCache::singleton()->clear(); - - // Remember to update newParserTests.php after changing the below - // (and it uses a slightly different syntax just for teh lulz) - $this->setupUploadDir(); - $user = User::createNew( 'WikiSysop' ); - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); - # note that the size/width/height/bits/etc of the file - # are actually set by inspecting the file itself; the arguments - # to recordUpload2 have no effect. That said, we try to make things - # match up so it is less confusing to readers of the code & tests. - $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', [ - 'size' => 7881, - 'width' => 1941, - 'height' => 220, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); - # again, note that size/width/height below are ignored; see above. - $image->recordUpload2( '', 'Upload of some lame thumbnail', 'Some lame thumbnail', [ - 'size' => 22589, - 'width' => 135, - 'height' => 135, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/png', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20130225203040' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); - $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ - 'size' => 12345, - 'width' => 240, - 'height' => 180, - 'bits' => 0, - 'media_type' => MEDIATYPE_DRAWING, - 'mime' => 'image/svg+xml', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - # This image will be blacklisted in [[MediaWiki:Bad image list]] - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); - $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 24, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); - $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 0, - 'media_type' => MEDIATYPE_VIDEO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Audio.oga' ) ); - $image->recordUpload2( '', 'An awesome hitsong', 'Will it play', [ - 'size' => 12345, - 'width' => 0, - 'height' => 0, - 'bits' => 0, - 'media_type' => MEDIATYPE_AUDIO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - # A DjVu file - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); - $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ - 'size' => 3249, - 'width' => 2480, - 'height' => 3508, - 'bits' => 0, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/vnd.djvu', - 'metadata' => ' - - - - - - - - - - - - - - - - - - - - - - - - -', - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123600' ), $user ); - } - - public function teardownDatabase() { - if ( !$this->databaseSetupDone ) { - $this->teardownGlobals(); - return; - } - $this->teardownUploadDir( $this->uploadDir ); - - $this->dbClone->destroy(); - $this->databaseSetupDone = false; - - if ( $this->useTemporaryTables ) { - if ( $this->db->getType() == 'sqlite' ) { - # Under SQLite the searchindex table is virtual and need - # to be explicitly destroyed. See bug 29912 - # See also MediaWikiTestCase::destroyDB() - wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" ); - $this->db->query( "DROP TABLE `parsertest_searchindex`" ); - } - # Don't need to do anything - $this->teardownGlobals(); - return; - } - - $tables = $this->listTables(); - - foreach ( $tables as $table ) { - if ( $this->db->getType() == 'oracle' ) { - $this->db->query( "DROP TABLE pt_$table DROP CONSTRAINTS" ); - } else { - $this->db->query( "DROP TABLE `parsertest_$table`" ); - } - } - - if ( $this->db->getType() == 'oracle' ) { - $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); - } - - $this->teardownGlobals(); - } - - /** - * Create a dummy uploads directory which will contain a couple - * of files in order to pass existence tests. - * - * @return string The directory - */ - private function setupUploadDir() { - global $IP; - - $dir = $this->uploadDir; - if ( $this->keepUploads && is_dir( $dir ) ) { - return; - } - - // wfDebug( "Creating upload directory $dir\n" ); - if ( file_exists( $dir ) ) { - wfDebug( "Already exists!\n" ); - return; - } - - wfMkdirParents( $dir . '/3/3a', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); - wfMkdirParents( $dir . '/e/ea', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/wiki.png", "$dir/e/ea/Thumb.png" ); - wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/0/09/Bad.jpg" ); - wfMkdirParents( $dir . '/f/ff', null, __METHOD__ ); - file_put_contents( "$dir/f/ff/Foobar.svg", - '' . - '' ); - wfMkdirParents( $dir . '/5/5f', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", "$dir/5/5f/LoremIpsum.djvu" ); - wfMkdirParents( $dir . '/0/00', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/320x240.ogv", "$dir/0/00/Video.ogv" ); - wfMkdirParents( $dir . '/4/41', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/media/say-test.ogg", "$dir/4/41/Audio.oga" ); - - return; - } - - /** - * Restore default values and perform any necessary clean-up - * after each test runs. - */ - public function teardownGlobals() { - RepoGroup::destroySingleton(); - FileBackendGroup::destroySingleton(); - LockManagerGroup::destroySingletons(); - LinkCache::singleton()->clear(); - MWTidy::destroySingleton(); - - foreach ( $this->savedGlobals as $var => $val ) { - $GLOBALS[$var] = $val; - } - } - - /** - * Remove the dummy uploads directory - * @param string $dir - */ - private function teardownUploadDir( $dir ) { - if ( $this->keepUploads ) { - return; - } - - // delete the files first, then the dirs. - self::deleteFiles( - [ - "$dir/3/3a/Foobar.jpg", - "$dir/thumb/3/3a/Foobar.jpg/*.jpg", - "$dir/e/ea/Thumb.png", - "$dir/0/09/Bad.jpg", - "$dir/5/5f/LoremIpsum.djvu", - "$dir/thumb/5/5f/LoremIpsum.djvu/*-LoremIpsum.djvu.jpg", - "$dir/f/ff/Foobar.svg", - "$dir/thumb/f/ff/Foobar.svg/*-Foobar.svg.png", - "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", - "$dir/0/00/Video.ogv", - "$dir/thumb/0/00/Video.ogv/120px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/180px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/240px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/270px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px-seek=2-Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px-seek=3.3666666666667-Video.ogv.jpg", - "$dir/4/41/Audio.oga", - ] - ); - - self::deleteDirs( - [ - "$dir/3/3a", - "$dir/3", - "$dir/thumb/3/3a/Foobar.jpg", - "$dir/thumb/3/3a", - "$dir/thumb/3", - "$dir/e/ea", - "$dir/e", - "$dir/f/ff/", - "$dir/f/", - "$dir/thumb/f/ff/Foobar.svg", - "$dir/thumb/f/ff/", - "$dir/thumb/f/", - "$dir/0/00/", - "$dir/0/09/", - "$dir/0/", - "$dir/5/5f", - "$dir/5", - "$dir/thumb/0/00/Video.ogv", - "$dir/thumb/0/00", - "$dir/thumb/0", - "$dir/thumb/5/5f/LoremIpsum.djvu", - "$dir/thumb/5/5f", - "$dir/thumb/5", - "$dir/thumb", - "$dir/4/41", - "$dir/4", - "$dir/math/f/a/5", - "$dir/math/f/a", - "$dir/math/f", - "$dir/math", - "$dir/lockdir", - "$dir", - ] - ); - } - - /** - * Delete the specified files, if they exist. - * @param array $files Full paths to files to delete. - */ - private static function deleteFiles( $files ) { - foreach ( $files as $pattern ) { - foreach ( glob( $pattern ) as $file ) { - if ( file_exists( $file ) ) { - unlink( $file ); - } - } - } - } - - /** - * Delete the specified directories, if they exist. Must be empty. - * @param array $dirs Full paths to directories to delete. - */ - private static function deleteDirs( $dirs ) { - foreach ( $dirs as $dir ) { - if ( is_dir( $dir ) ) { - rmdir( $dir ); - } - } - } - - /** - * "Running test $desc..." - * @param string $desc - */ - protected function showTesting( $desc ) { - print "Running test $desc... "; - } - - /** - * Print a happy success message. - * - * Refactored in 1.22 to use ParserTestResult - * - * @param ParserTestResult $testResult - * @return bool - */ - protected function showSuccess( ParserTestResult $testResult ) { - if ( $this->showProgress ) { - print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; - } - - return true; - } - - /** - * Print a failure message and provide some explanatory output - * about what went wrong if so configured. - * - * Refactored in 1.22 to use ParserTestResult - * - * @param ParserTestResult $testResult - * @return bool - */ - protected function showFailure( ParserTestResult $testResult ) { - if ( $this->showFailure ) { - if ( !$this->showProgress ) { - # In quiet mode we didn't show the 'Testing' message before the - # test, in case it succeeded. Show it now: - $this->showTesting( $testResult->description ); - } - - print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; - - if ( $this->showOutput ) { - print "--- Expected ---\n{$testResult->expected}\n"; - print "--- Actual ---\n{$testResult->actual}\n"; - } - - if ( $this->showDiffs ) { - print $this->quickDiff( $testResult->expected, $testResult->actual ); - if ( !$this->wellFormed( $testResult->actual ) ) { - print "XML error: $this->mXmlError\n"; - } - } - } - - return false; - } - - /** - * Print a skipped message. - * - * @return bool - */ - protected function showSkipped() { - if ( $this->showProgress ) { - print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n"; - } - - return true; - } - - /** - * Run given strings through a diff and return the (colorized) output. - * Requires writable /tmp directory and a 'diff' command in the PATH. - * - * @param string $input - * @param string $output - * @param string $inFileTail Tailing for the input file name - * @param string $outFileTail Tailing for the output file name - * @return string - */ - protected function quickDiff( $input, $output, - $inFileTail = 'expected', $outFileTail = 'actual' - ) { - if ( $this->markWhitespace ) { - $pairs = [ - "\n" => '¶', - ' ' => '·', - "\t" => '→' - ]; - $input = strtr( $input, $pairs ); - $output = strtr( $output, $pairs ); - } - - # Windows, or at least the fc utility, is retarded - $slash = wfIsWindows() ? '\\' : '/'; - $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand(); - - $infile = "$prefix-$inFileTail"; - $this->dumpToFile( $input, $infile ); - - $outfile = "$prefix-$outFileTail"; - $this->dumpToFile( $output, $outfile ); - - $shellInfile = wfEscapeShellArg( $infile ); - $shellOutfile = wfEscapeShellArg( $outfile ); - - global $wgDiff3; - // we assume that people with diff3 also have usual diff - if ( $this->useDwdiff ) { - $shellCommand = 'dwdiff -Pc'; - } else { - $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au'; - } - - $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" ); - - unlink( $infile ); - unlink( $outfile ); - - if ( $this->useDwdiff ) { - return $diff; - } else { - return $this->colorDiff( $diff ); - } - } - - /** - * Write the given string to a file, adding a final newline. - * - * @param string $data - * @param string $filename - */ - private function dumpToFile( $data, $filename ) { - $file = fopen( $filename, "wt" ); - fwrite( $file, $data . "\n" ); - fclose( $file ); - } - - /** - * Colorize unified diff output if set for ANSI color output. - * Subtractions are colored blue, additions red. - * - * @param string $text - * @return string - */ - protected function colorDiff( $text ) { - return preg_replace( - [ '/^(-.*)$/m', '/^(\+.*)$/m' ], - [ $this->term->color( 34 ) . '$1' . $this->term->reset(), - $this->term->color( 31 ) . '$1' . $this->term->reset() ], - $text ); - } - - /** - * Show "Reading tests from ..." - * - * @param string $path - */ - public function showRunFile( $path ) { - print $this->term->color( 1 ) . - "Reading tests from \"$path\"..." . - $this->term->reset() . - "\n"; - } - - /** - * Insert a temporary test article - * @param string $name The title, including any prefix - * @param string $text The article text - * @param int|string $line The input line number, for reporting errors - * @param bool|string $ignoreDuplicate Whether to silently ignore duplicate pages - * @throws Exception - * @throws MWException - */ - public static function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) { - global $wgCapitalLinks; - - $oldCapitalLinks = $wgCapitalLinks; - $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637 - - $text = self::chomp( $text ); - $name = self::chomp( $name ); - - $title = Title::newFromText( $name ); - - if ( is_null( $title ) ) { - throw new MWException( "invalid title '$name' at line $line\n" ); - } - - $page = WikiPage::factory( $title ); - $page->loadPageData( 'fromdbmaster' ); - - if ( $page->exists() ) { - if ( $ignoreDuplicate == 'ignoreduplicate' ) { - return; - } else { - throw new MWException( "duplicate article '$name' at line $line\n" ); - } - } - - $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW ); - - $wgCapitalLinks = $oldCapitalLinks; - } - - /** - * Steal a callback function from the primary parser, save it for - * application to our scary parser. If the hook is not installed, - * abort processing of this file. - * - * @param string $name - * @return bool True if tag hook is present - */ - public function requireHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mTagHooks[$name] ) ) { - $this->hooks[$name] = $wgParser->mTagHooks[$name]; - } else { - echo " This test suite requires the '$name' hook extension, skipping.\n"; - return false; - } - - return true; - } - - /** - * Steal a callback function from the primary parser, save it for - * application to our scary parser. If the hook is not installed, - * abort processing of this file. - * - * @param string $name - * @return bool True if function hook is present - */ - public function requireFunctionHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mFunctionHooks[$name] ) ) { - $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name]; - } else { - echo " This test suite requires the '$name' function hook extension, skipping.\n"; - return false; - } - - return true; - } - - /** - * Steal a callback function from the primary parser, save it for - * application to our scary parser. If the hook is not installed, - * abort processing of this file. - * - * @param string $name - * @return bool True if function hook is present - */ - public function requireTransparentHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) { - $this->transparentHooks[$name] = $wgParser->mTransparentTagHooks[$name]; - } else { - echo " This test suite requires the '$name' transparent hook extension, skipping.\n"; - return false; - } - - return true; - } - - private function wellFormed( $text ) { - $html = - Sanitizer::hackDocType() . - '' . - $text . - ''; - - $parser = xml_parser_create( "UTF-8" ); - - # case folding violates XML standard, turn it off - xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); - - if ( !xml_parse( $parser, $html, true ) ) { - $err = xml_error_string( xml_get_error_code( $parser ) ); - $position = xml_get_current_byte_index( $parser ); - $fragment = $this->extractFragment( $html, $position ); - $this->mXmlError = "$err at byte $position:\n$fragment"; - xml_parser_free( $parser ); - - return false; - } - - xml_parser_free( $parser ); - - return true; - } - - private function extractFragment( $text, $position ) { - $start = max( 0, $position - 10 ); - $before = $position - $start; - $fragment = '...' . - $this->term->color( 34 ) . - substr( $text, $start, $before ) . - $this->term->color( 0 ) . - $this->term->color( 31 ) . - $this->term->color( 1 ) . - substr( $text, $position, 1 ) . - $this->term->color( 0 ) . - $this->term->color( 34 ) . - substr( $text, $position + 1, 9 ) . - $this->term->color( 0 ) . - '...'; - $display = str_replace( "\n", ' ', $fragment ); - $caret = ' ' . - str_repeat( ' ', $before ) . - $this->term->color( 31 ) . - '^' . - $this->term->color( 0 ); - - return "$display\n$caret"; - } - - static function getFakeTimestamp( &$parser, &$ts ) { - $ts = 123; // parsed as '1970-01-01T00:02:03Z' - return true; - } -} diff --git a/tests/parser/ParserTestRunner.php b/tests/parser/ParserTestRunner.php new file mode 100644 index 0000000000..be3f0f7972 --- /dev/null +++ b/tests/parser/ParserTestRunner.php @@ -0,0 +1,1591 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @todo Make this more independent of the configuration (and if possible the database) + * @todo document + * @file + * @ingroup Testing + */ +use MediaWiki\MediaWikiServices; + +/** + * @ingroup Testing + */ +class ParserTestRunner { + /** + * @var bool $color whereas output should be colorized + */ + private $color; + + /** + * @var bool $showOutput Show test output + */ + private $showOutput; + + /** + * @var bool $useTemporaryTables Use temporary tables for the temporary database + */ + private $useTemporaryTables = true; + + /** + * @var bool $databaseSetupDone True if the database has been set up + */ + private $databaseSetupDone = false; + + /** + * Our connection to the database + * @var DatabaseBase + */ + private $db; + + /** + * Database clone helper + * @var CloneDatabase + */ + private $dbClone; + + /** + * @var DjVuSupport + */ + private $djVuSupport; + + /** + * @var TidySupport + */ + private $tidySupport; + + /** + * @var ITestRecorder + */ + private $recorder; + + private $uploadDir = null; + + public $regex = ""; + private $savedGlobals = []; + private $useDwdiff = false; + private $markWhitespace = false; + private $normalizationFunctions = []; + + /** + * Sets terminal colorization and diff/quick modes depending on OS and + * command-line options (--color and --quick). + * @param array $options + */ + public function __construct( $options = [] ) { + # Only colorize output if stdout is a terminal. + $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 ); + + if ( isset( $options['color'] ) ) { + switch ( $options['color'] ) { + case 'no': + $this->color = false; + break; + case 'yes': + default: + $this->color = true; + break; + } + } + + $this->term = $this->color + ? new AnsiTermColorer() + : new DummyTermColorer(); + + $this->showDiffs = !isset( $options['quick'] ); + $this->showProgress = !isset( $options['quiet'] ); + $this->showFailure = !( + isset( $options['quiet'] ) + && ( isset( $options['record'] ) + || isset( $options['compare'] ) ) ); // redundant output + + $this->showOutput = isset( $options['show-output'] ); + $this->useDwdiff = isset( $options['dwdiff'] ); + $this->markWhitespace = isset( $options['mark-ws'] ); + + if ( isset( $options['norm'] ) ) { + foreach ( explode( ',', $options['norm'] ) as $func ) { + if ( in_array( $func, [ 'removeTbody', 'trimWhitespace' ] ) ) { + $this->normalizationFunctions[] = $func; + } else { + echo "Warning: unknown normalization option \"$func\"\n"; + } + } + } + + if ( isset( $options['filter'] ) ) { + $options['regex'] = $options['filter']; + } + + if ( isset( $options['regex'] ) ) { + if ( isset( $options['record'] ) ) { + echo "Warning: --record cannot be used with --regex, disabling --record\n"; + unset( $options['record'] ); + } + $this->regex = $options['regex']; + } else { + # Matches anything + $this->regex = ''; + } + + $this->setupRecorder( $options ); + $this->keepUploads = isset( $options['keep-uploads'] ); + + if ( $this->keepUploads ) { + $this->uploadDir = wfTempDir() . '/mwParser-images'; + } else { + $this->uploadDir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; + } + + $this->runDisabled = isset( $options['run-disabled'] ); + $this->runParsoid = isset( $options['run-parsoid'] ); + + $this->djVuSupport = new DjVuSupport(); + $this->tidySupport = new TidySupport( isset( $options['use-tidy-config'] ) ); + if ( !$this->tidySupport->isEnabled() ) { + echo "Warning: tidy is not installed, skipping some tests\n"; + } + + $this->hooks = []; + $this->functionHooks = []; + $this->transparentHooks = []; + $this->setUp(); + } + + function setUp() { + global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, + $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, + $wgExtraNamespaces, $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, + $wgExtraInterlanguageLinkPrefixes, $wgLocalInterwikis, + $parserMemc, $wgThumbnailScriptPath, $wgScriptPath, $wgResourceBasePath, + $wgArticlePath, $wgScript, $wgStylePath, $wgExtensionAssetsPath, + $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers; + + $wgScriptPath = ''; + $wgScript = '/index.php'; + $wgStylePath = '/skins'; + $wgResourceBasePath = ''; + $wgExtensionAssetsPath = '/extensions'; + $wgArticlePath = '/wiki/$1'; + $wgThumbnailScriptPath = false; + $wgLockManagers = [ [ + 'name' => 'fsLockManager', + 'class' => 'FSLockManager', + 'lockDirectory' => $this->uploadDir . '/lockdir', + ], [ + 'name' => 'nullLockManager', + 'class' => 'NullLockManager', + ] ]; + $wgLocalFileRepo = [ + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => new FSFileBackend( [ + 'name' => 'local-backend', + 'wikiId' => wfWikiID(), + 'containerPaths' => [ + 'local-public' => $this->uploadDir . '/public', + 'local-thumb' => $this->uploadDir . '/thumb', + 'local-temp' => $this->uploadDir . '/temp', + 'local-deleted' => $this->uploadDir . '/deleted', + ] + ] ) + ]; + $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + # add a namespace shadowing a interwiki link, to test + # proper precedence when resolving links. (bug 51680) + $wgExtraNamespaces[100] = 'MemoryAlpha'; + $wgExtraNamespaces[101] = 'MemoryAlpha talk'; + + // XXX: tests won't run without this (for CACHE_DB) + if ( $wgMainCacheType === CACHE_DB ) { + $wgMainCacheType = CACHE_NONE; + } + if ( $wgMessageCacheType === CACHE_DB ) { + $wgMessageCacheType = CACHE_NONE; + } + if ( $wgParserCacheType === CACHE_DB ) { + $wgParserCacheType = CACHE_NONE; + } + + DeferredUpdates::clearPendingUpdates(); + $wgMemc = wfGetMainCache(); // checks $wgMainCacheType + $messageMemc = wfGetMessageCacheStorage(); + $parserMemc = wfGetParserCacheStorage(); + + RequestContext::resetMain(); + $context = new RequestContext; + $wgUser = new User; + $wgLang = $context->getLanguage(); + $wgOut = $context->getOutput(); + $wgRequest = $context->getRequest(); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], [ $wgParserConf ] ); + + if ( $wgStyleDirectory === false ) { + $wgStyleDirectory = "$IP/skins"; + } + + self::setupInterwikis(); + $wgLocalInterwikis = [ 'local', 'mi' ]; + // "extra language links" + // see https://gerrit.wikimedia.org/r/111390 + array_push( $wgExtraInterlanguageLinkPrefixes, 'mul' ); + + // Reset namespace cache + MWNamespace::getCanonicalNamespaces( true ); + Language::factory( 'en' )->resetNamespaces(); + } + + /** + * Insert hardcoded interwiki in the lookup table. + * + * This function insert a set of well known interwikis that are used in + * the parser tests. They can be considered has fixtures are injected in + * the interwiki cache by using the 'InterwikiLoadPrefix' hook. + * Since we are not interested in looking up interwikis in the database, + * the hook completely replace the existing mechanism (hook returns false). + */ + public static function setupInterwikis() { + # Hack: insert a few Wikipedia in-project interwiki prefixes, + # for testing inter-language links + Hooks::register( 'InterwikiLoadPrefix', function ( $prefix, &$iwData ) { + static $testInterwikis = [ + 'local' => [ + 'iw_url' => 'http://doesnt.matter.org/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ], + 'wikipedia' => [ + 'iw_url' => 'http://en.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ], + 'meatball' => [ + 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ], + 'memoryalpha' => [ + 'iw_url' => 'http://www.memory-alpha.org/en/index.php/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0 ], + 'zh' => [ + 'iw_url' => 'http://zh.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + 'es' => [ + 'iw_url' => 'http://es.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + 'fr' => [ + 'iw_url' => 'http://fr.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + 'ru' => [ + 'iw_url' => 'http://ru.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + 'mi' => [ + 'iw_url' => 'http://mi.wikipedia.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + 'mul' => [ + 'iw_url' => 'http://wikisource.org/wiki/$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 1 ], + ]; + if ( array_key_exists( $prefix, $testInterwikis ) ) { + $iwData = $testInterwikis[$prefix]; + } + + // We only want to rely on the above fixtures + return false; + } );// hooks::register + } + + /** + * Remove the hardcoded interwiki lookup table. + */ + public static function tearDownInterwikis() { + Hooks::clear( 'InterwikiLoadPrefix' ); + } + + /** + * Reset the Title-related services that need resetting + * for each test + */ + public static function resetTitleServices() { + $services = MediaWikiServices::getInstance(); + $services->resetServiceForTesting( 'TitleFormatter' ); + $services->resetServiceForTesting( 'TitleParser' ); + $services->resetServiceForTesting( '_MediaWikiTitleCodec' ); + $services->resetServiceForTesting( 'LinkRenderer' ); + $services->resetServiceForTesting( 'LinkRendererFactory' ); + } + + public function setupRecorder( $options ) { + if ( isset( $options['record'] ) ) { + $this->recorder = new DbTestRecorder( $this ); + $this->recorder->version = isset( $options['setversion'] ) ? + $options['setversion'] : SpecialVersion::getVersion(); + } elseif ( isset( $options['compare'] ) ) { + $this->recorder = new DbTestPreviewer( $this ); + } else { + $this->recorder = new TestRecorder( $this ); + } + } + + /** + * Remove last character if it is a newline + * @group utility + * @param string $s + * @return string + */ + public static function chomp( $s ) { + if ( substr( $s, -1 ) === "\n" ) { + return substr( $s, 0, -1 ); + } else { + return $s; + } + } + + /** + * Run a series of tests listed in the given text files. + * Each test consists of a brief description, wikitext input, + * and the expected HTML output. + * + * Prints status updates on stdout and counts up the total + * number and percentage of passed tests. + * + * @param array $filenames Array of strings + * @return bool True if passed all tests, false if any tests failed. + */ + public function runTestsFromFiles( $filenames ) { + $ok = false; + + // be sure, ParserTestRunner::addArticle has correct language set, + // so that system messages gets into the right language cache + $GLOBALS['wgLanguageCode'] = 'en'; + $GLOBALS['wgContLang'] = Language::factory( 'en' ); + + $this->recorder->start(); + try { + $this->setupDatabase(); + $ok = true; + + foreach ( $filenames as $filename ) { + echo "Running parser tests from: $filename\n"; + $tests = new TestFileReader( $filename, $this ); + $ok = $this->runTests( $tests ) && $ok; + } + + $this->teardownDatabase(); + $this->recorder->report(); + } catch ( DBError $e ) { + echo $e->getMessage(); + } + $this->recorder->end(); + + return $ok; + } + + function runTests( $tests ) { + $ok = true; + + foreach ( $tests as $t ) { + $result = + $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] ); + $ok = $ok && $result; + $this->recorder->record( $t['test'], $t['subtest'], $result ); + } + + if ( $this->showProgress ) { + print "\n"; + } + + return $ok; + } + + /** + * Get a Parser object + * + * @param string $preprocessor + * @return Parser + */ + function getParser( $preprocessor = null ) { + global $wgParserConf; + + $class = $wgParserConf['class']; + $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); + + foreach ( $this->hooks as $tag => $callback ) { + $parser->setHook( $tag, $callback ); + } + + foreach ( $this->functionHooks as $tag => $bits ) { + list( $callback, $flags ) = $bits; + $parser->setFunctionHook( $tag, $callback, $flags ); + } + + foreach ( $this->transparentHooks as $tag => $callback ) { + $parser->setTransparentTagHook( $tag, $callback ); + } + + Hooks::run( 'ParserTestParser', [ &$parser ] ); + + return $parser; + } + + /** + * Run a given wikitext input through a freshly-constructed wiki parser, + * and compare the output against the expected results. + * Prints status and explanatory messages to stdout. + * + * @param string $desc Test's description + * @param string $input Wikitext to try rendering + * @param string $result Result to output + * @param array $opts Test's options + * @param string $config Overrides for global variables, one per line + * @return bool + */ + public function runTest( $desc, $input, $result, $opts, $config ) { + if ( $this->showProgress ) { + $this->showTesting( $desc ); + } + + $opts = $this->parseOptions( $opts ); + $context = $this->setupGlobals( $opts, $config ); + + $user = $context->getUser(); + $options = ParserOptions::newFromContext( $context ); + + if ( isset( $opts['djvu'] ) ) { + if ( !$this->djVuSupport->isEnabled() ) { + return $this->showSkipped(); + } + } + + if ( isset( $opts['tidy'] ) ) { + if ( !$this->tidySupport->isEnabled() ) { + return $this->showSkipped(); + } else { + $options->setTidy( true ); + } + } + + if ( isset( $opts['title'] ) ) { + $titleText = $opts['title']; + } else { + $titleText = 'Parser test'; + } + + ObjectCache::getMainWANInstance()->clearProcessCache(); + $local = isset( $opts['local'] ); + $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; + $parser = $this->getParser( $preprocessor ); + $title = Title::newFromText( $titleText ); + + if ( isset( $opts['pst'] ) ) { + $out = $parser->preSaveTransform( $input, $title, $user, $options ); + } elseif ( isset( $opts['msg'] ) ) { + $out = $parser->transformMsg( $input, $options, $title ); + } elseif ( isset( $opts['section'] ) ) { + $section = $opts['section']; + $out = $parser->getSection( $input, $section ); + } elseif ( isset( $opts['replace'] ) ) { + $section = $opts['replace'][0]; + $replace = $opts['replace'][1]; + $out = $parser->replaceSection( $input, $section, $replace ); + } elseif ( isset( $opts['comment'] ) ) { + $out = Linker::formatComment( $input, $title, $local ); + } elseif ( isset( $opts['preload'] ) ) { + $out = $parser->getPreloadText( $input, $title, $options ); + } else { + $output = $parser->parse( $input, $title, $options, true, true, 1337 ); + $output->setTOCEnabled( !isset( $opts['notoc'] ) ); + $out = $output->getText(); + if ( isset( $opts['tidy'] ) ) { + $out = preg_replace( '/\s+$/', '', $out ); + } + + if ( isset( $opts['showtitle'] ) ) { + if ( $output->getTitleText() ) { + $title = $output->getTitleText(); + } + + $out = "$title\n$out"; + } + + if ( isset( $opts['showindicators'] ) ) { + $indicators = ''; + foreach ( $output->getIndicators() as $id => $content ) { + $indicators .= "$id=$content\n"; + } + $out = $indicators . $out; + } + + if ( isset( $opts['ill'] ) ) { + $out = implode( ' ', $output->getLanguageLinks() ); + } elseif ( isset( $opts['cat'] ) ) { + $outputPage = $context->getOutput(); + $outputPage->addCategoryLinks( $output->getCategories() ); + $cats = $outputPage->getCategoryLinks(); + + if ( isset( $cats['normal'] ) ) { + $out = implode( ' ', $cats['normal'] ); + } else { + $out = ''; + } + } + } + + $this->teardownGlobals(); + + if ( count( $this->normalizationFunctions ) ) { + $result = ParserTestResultNormalizer::normalize( $result, $this->normalizationFunctions ); + $out = ParserTestResultNormalizer::normalize( $out, $this->normalizationFunctions ); + } + + $testResult = new ParserTestResult( $desc ); + $testResult->expected = $result; + $testResult->actual = $out; + + return $this->showTestResult( $testResult ); + } + + /** + * Refactored in 1.22 to use ParserTestResult + * @param ParserTestResult $testResult + * @return bool + */ + function showTestResult( ParserTestResult $testResult ) { + if ( $testResult->isSuccess() ) { + $this->showSuccess( $testResult ); + return true; + } else { + $this->showFailure( $testResult ); + return false; + } + } + + /** + * Use a regex to find out the value of an option + * @param string $key Name of option val to retrieve + * @param array $opts Options array to look in + * @param mixed $default Default value returned if not found + * @return mixed + */ + private static function getOptionValue( $key, $opts, $default ) { + $key = strtolower( $key ); + + if ( isset( $opts[$key] ) ) { + return $opts[$key]; + } else { + return $default; + } + } + + private function parseOptions( $instring ) { + $opts = []; + // foo + // foo=bar + // foo="bar baz" + // foo=[[bar baz]] + // foo=bar,"baz quux" + // foo={...json...} + $defs = '(?(DEFINE) + (? # Quoted string + " + (?:[^\\\\"] | \\\\.)* + " + ) + (? + \{ # Open bracket + (?: + [^"{}] | # Not a quoted string or object, or + (?&qstr) | # A quoted string, or + (?&json) # A json object (recursively) + )* + \} # Close bracket + ) + (? + (?: + (?&qstr) # Quoted val + | + \[\[ + [^]]* # Link target + \]\] + | + [\w-]+ # Plain word + | + (?&json) # JSON object + ) + ) + )'; + $regex = '/' . $defs . '\b + (?[\w-]+) # Key + \b + (?:\s* + = # First sub-value + \s* + (? + (?&value) + (?:\s* + , # Sub-vals 1..N + \s* + (?&value) + )* + ) + )? + /x'; + $valueregex = '/' . $defs . '(?&value)/x'; + + if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $bits ) { + $key = strtolower( $bits['k'] ); + if ( !isset( $bits['v'] ) ) { + $opts[$key] = true; + } else { + preg_match_all( $valueregex, $bits['v'], $vmatches ); + $opts[$key] = array_map( [ $this, 'cleanupOption' ], $vmatches[0] ); + if ( count( $opts[$key] ) == 1 ) { + $opts[$key] = $opts[$key][0]; + } + } + } + } + return $opts; + } + + private function cleanupOption( $opt ) { + if ( substr( $opt, 0, 1 ) == '"' ) { + return stripcslashes( substr( $opt, 1, -1 ) ); + } + + if ( substr( $opt, 0, 2 ) == '[[' ) { + return substr( $opt, 2, -2 ); + } + + if ( substr( $opt, 0, 1 ) == '{' ) { + return FormatJson::decode( $opt, true ); + } + return $opt; + } + + /** + * Set up the global variables for a consistent environment for each test. + * Ideally this should replace the global configuration entirely. + * @param string $opts + * @param string $config + * @return RequestContext + */ + public function setupGlobals( $opts = '', $config = '' ) { + # Find out values for some special options. + $lang = + self::getOptionValue( 'language', $opts, 'en' ); + $variant = + self::getOptionValue( 'variant', $opts, false ); + $maxtoclevel = + self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); + $linkHolderBatchSize = + self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); + + $settings = [ + 'wgServer' => 'http://example.org', + 'wgServerName' => 'example.org', + 'wgScript' => '/index.php', + 'wgScriptPath' => '', + 'wgArticlePath' => '/wiki/$1', + 'wgActionPaths' => [], + 'wgLockManagers' => [ [ + 'name' => 'fsLockManager', + 'class' => 'FSLockManager', + 'lockDirectory' => $this->uploadDir . '/lockdir', + ], [ + 'name' => 'nullLockManager', + 'class' => 'NullLockManager', + ] ], + 'wgLocalFileRepo' => [ + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => new FSFileBackend( [ + 'name' => 'local-backend', + 'wikiId' => wfWikiID(), + 'containerPaths' => [ + 'local-public' => $this->uploadDir, + 'local-thumb' => $this->uploadDir . '/thumb', + 'local-temp' => $this->uploadDir . '/temp', + 'local-deleted' => $this->uploadDir . '/delete', + ] + ] ) + ], + 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), + 'wgUploadNavigationUrl' => false, + 'wgStylePath' => '/skins', + 'wgSitename' => 'MediaWiki', + 'wgLanguageCode' => $lang, + 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_', + 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), + 'wgLang' => null, + 'wgContLang' => null, + 'wgNamespacesWithSubpages' => [ 0 => isset( $opts['subpage'] ) ], + 'wgMaxTocLevel' => $maxtoclevel, + 'wgCapitalLinks' => true, + 'wgNoFollowLinks' => true, + 'wgNoFollowDomainExceptions' => [ 'no-nofollow.org' ], + 'wgThumbnailScriptPath' => false, + 'wgUseImageResize' => true, + 'wgSVGConverter' => 'null', + 'wgSVGConverters' => [ 'null' => 'echo "1">$output' ], + 'wgLocaltimezone' => 'UTC', + 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), + 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], + 'wgDefaultLanguageVariant' => $variant, + 'wgVariantArticlePath' => false, + 'wgGroupPermissions' => [ '*' => [ + 'createaccount' => true, + 'read' => true, + 'edit' => true, + 'createpage' => true, + 'createtalk' => true, + ] ], + 'wgNamespaceProtection' => [ NS_MEDIAWIKI => 'editinterface' ], + 'wgDefaultExternalStore' => [], + 'wgForeignFileRepos' => [], + 'wgLinkHolderBatchSize' => $linkHolderBatchSize, + 'wgExperimentalHtmlIds' => false, + 'wgExternalLinkTarget' => false, + 'wgHtml5' => true, + 'wgAdaptiveMessageCache' => true, + 'wgDisableLangConversion' => false, + 'wgDisableTitleConversion' => false, + // Tidy options. + 'wgUseTidy' => false, + 'wgTidyConfig' => isset( $opts['tidy'] ) ? $this->tidySupport->getConfig() : null + ]; + + if ( $config ) { + $configLines = explode( "\n", $config ); + + foreach ( $configLines as $line ) { + list( $var, $value ) = explode( '=', $line, 2 ); + + $settings[$var] = eval( "return $value;" ); + } + } + + $this->savedGlobals = []; + + /** @since 1.20 */ + Hooks::run( 'ParserTestGlobals', [ &$settings ] ); + + foreach ( $settings as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + + $GLOBALS[$var] = $val; + } + + // Must be set before $context as user language defaults to $wgContLang + $GLOBALS['wgContLang'] = Language::factory( $lang ); + $GLOBALS['wgMemc'] = new EmptyBagOStuff; + + RequestContext::resetMain(); + $context = RequestContext::getMain(); + $GLOBALS['wgLang'] = $context->getLanguage(); + $GLOBALS['wgOut'] = $context->getOutput(); + $GLOBALS['wgUser'] = $context->getUser(); + + // We (re)set $wgThumbLimits to a single-element array above. + $context->getUser()->setOption( 'thumbsize', 0 ); + + global $wgHooks; + + $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; + $wgHooks['ParserGetVariableValueTs'][] = 'ParserTestRunner::getFakeTimestamp'; + + MagicWord::clearCache(); + MWTidy::destroySingleton(); + RepoGroup::destroySingleton(); + + self::resetTitleServices(); + + return $context; + } + + /** + * List of temporary tables to create, without prefix. + * Some of these probably aren't necessary. + * @return array + */ + private function listTables() { + $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions', + 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks', + 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks', + 'site_stats', 'ipblocks', 'image', 'oldimage', + 'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search', + 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo', + 'archive', 'user_groups', 'page_props', 'category' + ]; + + if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) { + array_push( $tables, 'searchindex' ); + } + + // Allow extensions to add to the list of tables to duplicate; + // may be necessary if they hook into page save or other code + // which will require them while running tests. + Hooks::run( 'ParserTestTables', [ &$tables ] ); + + return $tables; + } + + /** + * Set up a temporary set of wiki tables to work with for the tests. + * Currently this will only be done once per run, and any changes to + * the db will be visible to later tests in the run. + */ + public function setupDatabase() { + global $wgDBprefix; + + if ( $this->databaseSetupDone ) { + return; + } + + $this->db = wfGetDB( DB_MASTER ); + $dbType = $this->db->getType(); + + if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) { + throw new MWException( 'setupDatabase should be called before setupGlobals' ); + } + + $this->databaseSetupDone = true; + + # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892). + # It seems to have been fixed since (r55079?), but regressed at some point before r85701. + # This works around it for now... + ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; + + # CREATE TEMPORARY TABLE breaks if there is more than one server + if ( wfGetLB()->getServerCount() != 1 ) { + $this->useTemporaryTables = false; + } + + $temporary = $this->useTemporaryTables || $dbType == 'postgres'; + $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_'; + + $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix ); + $this->dbClone->useTemporaryTables( $temporary ); + $this->dbClone->cloneTableStructure(); + + if ( $dbType == 'oracle' ) { + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + # Insert 0 user to prevent FK violations + + # Anonymous user + $this->db->insert( 'user', [ + 'user_id' => 0, + 'user_name' => 'Anonymous' ] ); + } + + # Update certain things in site_stats + $this->db->insert( 'site_stats', + [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ] ); + + # Reinitialise the LocalisationCache to match the database state + Language::getLocalisationCache()->unloadAll(); + + # Clear the message cache + MessageCache::singleton()->clear(); + + // Remember to update newParserTests.php after changing the below + // (and it uses a slightly different syntax just for teh lulz) + $this->setupUploadDir(); + $user = User::createNew( 'WikiSysop' ); + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); + # note that the size/width/height/bits/etc of the file + # are actually set by inspecting the file itself; the arguments + # to recordUpload2 have no effect. That said, we try to make things + # match up so it is less confusing to readers of the code & tests. + $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', [ + 'size' => 7881, + 'width' => 1941, + 'height' => 220, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); + # again, note that size/width/height below are ignored; see above. + $image->recordUpload2( '', 'Upload of some lame thumbnail', 'Some lame thumbnail', [ + 'size' => 22589, + 'width' => 135, + 'height' => 135, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/png', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20130225203040' ), $user ); + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); + $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ + 'size' => 12345, + 'width' => 240, + 'height' => 180, + 'bits' => 0, + 'media_type' => MEDIATYPE_DRAWING, + 'mime' => 'image/svg+xml', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + + # This image will be blacklisted in [[MediaWiki:Bad image list]] + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); + $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', [ + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 24, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); + $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 0, + 'media_type' => MEDIATYPE_VIDEO, + 'mime' => 'application/ogg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Audio.oga' ) ); + $image->recordUpload2( '', 'An awesome hitsong', 'Will it play', [ + 'size' => 12345, + 'width' => 0, + 'height' => 0, + 'bits' => 0, + 'media_type' => MEDIATYPE_AUDIO, + 'mime' => 'application/ogg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + + # A DjVu file + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); + $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ + 'size' => 3249, + 'width' => 2480, + 'height' => 3508, + 'bits' => 0, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/vnd.djvu', + 'metadata' => ' + + + + + + + + + + + + + + + + + + + + + + + + +', + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123600' ), $user ); + } + + public function teardownDatabase() { + if ( !$this->databaseSetupDone ) { + $this->teardownGlobals(); + return; + } + $this->teardownUploadDir( $this->uploadDir ); + + $this->dbClone->destroy(); + $this->databaseSetupDone = false; + + if ( $this->useTemporaryTables ) { + if ( $this->db->getType() == 'sqlite' ) { + # Under SQLite the searchindex table is virtual and need + # to be explicitly destroyed. See bug 29912 + # See also MediaWikiTestCase::destroyDB() + wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" ); + $this->db->query( "DROP TABLE `parsertest_searchindex`" ); + } + # Don't need to do anything + $this->teardownGlobals(); + return; + } + + $tables = $this->listTables(); + + foreach ( $tables as $table ) { + if ( $this->db->getType() == 'oracle' ) { + $this->db->query( "DROP TABLE pt_$table DROP CONSTRAINTS" ); + } else { + $this->db->query( "DROP TABLE `parsertest_$table`" ); + } + } + + if ( $this->db->getType() == 'oracle' ) { + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + } + + $this->teardownGlobals(); + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return string The directory + */ + private function setupUploadDir() { + global $IP; + + $dir = $this->uploadDir; + if ( $this->keepUploads && is_dir( $dir ) ) { + return; + } + + // wfDebug( "Creating upload directory $dir\n" ); + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + return; + } + + wfMkdirParents( $dir . '/3/3a', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); + wfMkdirParents( $dir . '/e/ea', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/parser/wiki.png", "$dir/e/ea/Thumb.png" ); + wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/0/09/Bad.jpg" ); + wfMkdirParents( $dir . '/f/ff', null, __METHOD__ ); + file_put_contents( "$dir/f/ff/Foobar.svg", + '' . + '' ); + wfMkdirParents( $dir . '/5/5f', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", "$dir/5/5f/LoremIpsum.djvu" ); + wfMkdirParents( $dir . '/0/00', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/parser/320x240.ogv", "$dir/0/00/Video.ogv" ); + wfMkdirParents( $dir . '/4/41', null, __METHOD__ ); + copy( "$IP/tests/phpunit/data/media/say-test.ogg", "$dir/4/41/Audio.oga" ); + + return; + } + + /** + * Restore default values and perform any necessary clean-up + * after each test runs. + */ + public function teardownGlobals() { + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + LockManagerGroup::destroySingletons(); + LinkCache::singleton()->clear(); + MWTidy::destroySingleton(); + + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + } + + /** + * Remove the dummy uploads directory + * @param string $dir + */ + private function teardownUploadDir( $dir ) { + if ( $this->keepUploads ) { + return; + } + + // delete the files first, then the dirs. + self::deleteFiles( + [ + "$dir/3/3a/Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/*.jpg", + "$dir/e/ea/Thumb.png", + "$dir/0/09/Bad.jpg", + "$dir/5/5f/LoremIpsum.djvu", + "$dir/thumb/5/5f/LoremIpsum.djvu/*-LoremIpsum.djvu.jpg", + "$dir/f/ff/Foobar.svg", + "$dir/thumb/f/ff/Foobar.svg/*-Foobar.svg.png", + "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", + "$dir/0/00/Video.ogv", + "$dir/thumb/0/00/Video.ogv/120px--Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/180px--Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/240px--Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/320px--Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/270px--Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/320px-seek=2-Video.ogv.jpg", + "$dir/thumb/0/00/Video.ogv/320px-seek=3.3666666666667-Video.ogv.jpg", + "$dir/4/41/Audio.oga", + ] + ); + + self::deleteDirs( + [ + "$dir/3/3a", + "$dir/3", + "$dir/thumb/3/3a/Foobar.jpg", + "$dir/thumb/3/3a", + "$dir/thumb/3", + "$dir/e/ea", + "$dir/e", + "$dir/f/ff/", + "$dir/f/", + "$dir/thumb/f/ff/Foobar.svg", + "$dir/thumb/f/ff/", + "$dir/thumb/f/", + "$dir/0/00/", + "$dir/0/09/", + "$dir/0/", + "$dir/5/5f", + "$dir/5", + "$dir/thumb/0/00/Video.ogv", + "$dir/thumb/0/00", + "$dir/thumb/0", + "$dir/thumb/5/5f/LoremIpsum.djvu", + "$dir/thumb/5/5f", + "$dir/thumb/5", + "$dir/thumb", + "$dir/4/41", + "$dir/4", + "$dir/math/f/a/5", + "$dir/math/f/a", + "$dir/math/f", + "$dir/math", + "$dir/lockdir", + "$dir", + ] + ); + } + + /** + * Delete the specified files, if they exist. + * @param array $files Full paths to files to delete. + */ + private static function deleteFiles( $files ) { + foreach ( $files as $pattern ) { + foreach ( glob( $pattern ) as $file ) { + if ( file_exists( $file ) ) { + unlink( $file ); + } + } + } + } + + /** + * Delete the specified directories, if they exist. Must be empty. + * @param array $dirs Full paths to directories to delete. + */ + private static function deleteDirs( $dirs ) { + foreach ( $dirs as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); + } + } + } + + /** + * "Running test $desc..." + * @param string $desc + */ + protected function showTesting( $desc ) { + print "Running test $desc... "; + } + + /** + * Print a happy success message. + * + * Refactored in 1.22 to use ParserTestResult + * + * @param ParserTestResult $testResult + * @return bool + */ + protected function showSuccess( ParserTestResult $testResult ) { + if ( $this->showProgress ) { + print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; + } + + return true; + } + + /** + * Print a failure message and provide some explanatory output + * about what went wrong if so configured. + * + * Refactored in 1.22 to use ParserTestResult + * + * @param ParserTestResult $testResult + * @return bool + */ + protected function showFailure( ParserTestResult $testResult ) { + if ( $this->showFailure ) { + if ( !$this->showProgress ) { + # In quiet mode we didn't show the 'Testing' message before the + # test, in case it succeeded. Show it now: + $this->showTesting( $testResult->description ); + } + + print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; + + if ( $this->showOutput ) { + print "--- Expected ---\n{$testResult->expected}\n"; + print "--- Actual ---\n{$testResult->actual}\n"; + } + + if ( $this->showDiffs ) { + print $this->quickDiff( $testResult->expected, $testResult->actual ); + if ( !$this->wellFormed( $testResult->actual ) ) { + print "XML error: $this->mXmlError\n"; + } + } + } + + return false; + } + + /** + * Print a skipped message. + * + * @return bool + */ + protected function showSkipped() { + if ( $this->showProgress ) { + print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n"; + } + + return true; + } + + /** + * Run given strings through a diff and return the (colorized) output. + * Requires writable /tmp directory and a 'diff' command in the PATH. + * + * @param string $input + * @param string $output + * @param string $inFileTail Tailing for the input file name + * @param string $outFileTail Tailing for the output file name + * @return string + */ + protected function quickDiff( $input, $output, + $inFileTail = 'expected', $outFileTail = 'actual' + ) { + if ( $this->markWhitespace ) { + $pairs = [ + "\n" => '¶', + ' ' => '·', + "\t" => '→' + ]; + $input = strtr( $input, $pairs ); + $output = strtr( $output, $pairs ); + } + + # Windows, or at least the fc utility, is retarded + $slash = wfIsWindows() ? '\\' : '/'; + $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand(); + + $infile = "$prefix-$inFileTail"; + $this->dumpToFile( $input, $infile ); + + $outfile = "$prefix-$outFileTail"; + $this->dumpToFile( $output, $outfile ); + + $shellInfile = wfEscapeShellArg( $infile ); + $shellOutfile = wfEscapeShellArg( $outfile ); + + global $wgDiff3; + // we assume that people with diff3 also have usual diff + if ( $this->useDwdiff ) { + $shellCommand = 'dwdiff -Pc'; + } else { + $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au'; + } + + $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" ); + + unlink( $infile ); + unlink( $outfile ); + + if ( $this->useDwdiff ) { + return $diff; + } else { + return $this->colorDiff( $diff ); + } + } + + /** + * Write the given string to a file, adding a final newline. + * + * @param string $data + * @param string $filename + */ + private function dumpToFile( $data, $filename ) { + $file = fopen( $filename, "wt" ); + fwrite( $file, $data . "\n" ); + fclose( $file ); + } + + /** + * Colorize unified diff output if set for ANSI color output. + * Subtractions are colored blue, additions red. + * + * @param string $text + * @return string + */ + protected function colorDiff( $text ) { + return preg_replace( + [ '/^(-.*)$/m', '/^(\+.*)$/m' ], + [ $this->term->color( 34 ) . '$1' . $this->term->reset(), + $this->term->color( 31 ) . '$1' . $this->term->reset() ], + $text ); + } + + /** + * Show "Reading tests from ..." + * + * @param string $path + */ + public function showRunFile( $path ) { + print $this->term->color( 1 ) . + "Reading tests from \"$path\"..." . + $this->term->reset() . + "\n"; + } + + /** + * Insert a temporary test article + * @param string $name The title, including any prefix + * @param string $text The article text + * @param int|string $line The input line number, for reporting errors + * @param bool|string $ignoreDuplicate Whether to silently ignore duplicate pages + * @throws Exception + * @throws MWException + */ + public static function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) { + global $wgCapitalLinks; + + $oldCapitalLinks = $wgCapitalLinks; + $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637 + + $text = self::chomp( $text ); + $name = self::chomp( $name ); + + $title = Title::newFromText( $name ); + + if ( is_null( $title ) ) { + throw new MWException( "invalid title '$name' at line $line\n" ); + } + + $page = WikiPage::factory( $title ); + $page->loadPageData( 'fromdbmaster' ); + + if ( $page->exists() ) { + if ( $ignoreDuplicate == 'ignoreduplicate' ) { + return; + } else { + throw new MWException( "duplicate article '$name' at line $line\n" ); + } + } + + $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW ); + + $wgCapitalLinks = $oldCapitalLinks; + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param string $name + * @return bool True if tag hook is present + */ + public function requireHook( $name ) { + global $wgParser; + + $wgParser->firstCallInit(); // make sure hooks are loaded. + + if ( isset( $wgParser->mTagHooks[$name] ) ) { + $this->hooks[$name] = $wgParser->mTagHooks[$name]; + } else { + echo " This test suite requires the '$name' hook extension, skipping.\n"; + return false; + } + + return true; + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param string $name + * @return bool True if function hook is present + */ + public function requireFunctionHook( $name ) { + global $wgParser; + + $wgParser->firstCallInit(); // make sure hooks are loaded. + + if ( isset( $wgParser->mFunctionHooks[$name] ) ) { + $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name]; + } else { + echo " This test suite requires the '$name' function hook extension, skipping.\n"; + return false; + } + + return true; + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param string $name + * @return bool True if function hook is present + */ + public function requireTransparentHook( $name ) { + global $wgParser; + + $wgParser->firstCallInit(); // make sure hooks are loaded. + + if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) { + $this->transparentHooks[$name] = $wgParser->mTransparentTagHooks[$name]; + } else { + echo " This test suite requires the '$name' transparent hook extension, skipping.\n"; + return false; + } + + return true; + } + + private function wellFormed( $text ) { + $html = + Sanitizer::hackDocType() . + '' . + $text . + ''; + + $parser = xml_parser_create( "UTF-8" ); + + # case folding violates XML standard, turn it off + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + if ( !xml_parse( $parser, $html, true ) ) { + $err = xml_error_string( xml_get_error_code( $parser ) ); + $position = xml_get_current_byte_index( $parser ); + $fragment = $this->extractFragment( $html, $position ); + $this->mXmlError = "$err at byte $position:\n$fragment"; + xml_parser_free( $parser ); + + return false; + } + + xml_parser_free( $parser ); + + return true; + } + + private function extractFragment( $text, $position ) { + $start = max( 0, $position - 10 ); + $before = $position - $start; + $fragment = '...' . + $this->term->color( 34 ) . + substr( $text, $start, $before ) . + $this->term->color( 0 ) . + $this->term->color( 31 ) . + $this->term->color( 1 ) . + substr( $text, $position, 1 ) . + $this->term->color( 0 ) . + $this->term->color( 34 ) . + substr( $text, $position + 1, 9 ) . + $this->term->color( 0 ) . + '...'; + $display = str_replace( "\n", ' ', $fragment ); + $caret = ' ' . + str_repeat( ' ', $before ) . + $this->term->color( 31 ) . + '^' . + $this->term->color( 0 ); + + return "$display\n$caret"; + } + + static function getFakeTimestamp( &$parser, &$ts ) { + $ts = 123; // parsed as '1970-01-01T00:02:03Z' + return true; + } +} diff --git a/tests/parser/TestFileDataProvider.php b/tests/parser/TestFileDataProvider.php index 00b1f3f01c..5528605968 100644 --- a/tests/parser/TestFileDataProvider.php +++ b/tests/parser/TestFileDataProvider.php @@ -21,9 +21,9 @@ /** * An iterator for use as a phpunit data provider. Provides the test arguments - * in the order expected by NewParserTest::testParserTest(). + * in the order expected by ParserIntegrationTest::testParserTest(). */ -class TestFileDataProvider extends TestFileIterator { +class TestFileDataProvider extends TestFileReader { function current() { $test = parent::current(); if ( $test ) { diff --git a/tests/parser/TestFileIterator.php b/tests/parser/TestFileIterator.php deleted file mode 100644 index 731d35c775..0000000000 --- a/tests/parser/TestFileIterator.php +++ /dev/null @@ -1,324 +0,0 @@ -file = $file; - $this->fh = fopen( $this->file, "rt" ); - - if ( !$this->fh ) { - throw new MWException( "Couldn't open file '$file'\n" ); - } - - $this->parserTest = $parserTest; - $this->delayedParserTest = new DelayedParserTest(); - - $this->lineNum = $this->index = 0; - } - - function rewind() { - if ( fseek( $this->fh, 0 ) ) { - throw new MWException( "Couldn't fseek to the start of '$this->file'\n" ); - } - - $this->index = -1; - $this->lineNum = 0; - $this->eof = false; - $this->next(); - - return true; - } - - function current() { - return $this->test; - } - - function key() { - return $this->index; - } - - function next() { - if ( $this->readNextTest() ) { - $this->index++; - return true; - } else { - $this->eof = true; - } - } - - function valid() { - return $this->eof != true; - } - - function setupCurrentTest() { - // "input" and "result" are old section names allowed - // for backwards-compatibility. - $input = $this->checkSection( [ 'wikitext', 'input' ], false ); - $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false ); - // some tests have "with tidy" and "without tidy" variants - $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false ); - if ( $tidy != false ) { - if ( $this->nextSubTest == 0 ) { - if ( $result != false ) { - $this->nextSubTest = 1; // rerun non-tidy variant later - } - $result = $tidy; - } else { - $this->nextSubTest = 0; // go on to next test after this - $tidy = false; - } - } - - if ( !isset( $this->sectionData['options'] ) ) { - $this->sectionData['options'] = ''; - } - - if ( !isset( $this->sectionData['config'] ) ) { - $this->sectionData['config'] = ''; - } - - $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && - !$this->parserTest->runDisabled; - $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && - $result == 'html' && - !$this->parserTest->runParsoid; - $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] ); - if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) { - # disabled test - return false; - } - - # We are really going to run the test, run pending hooks and hooks function - wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" ); - $hooksResult = $this->delayedParserTest->unleash( $this->parserTest ); - if ( !$hooksResult ) { - # Some hook reported an issue. Abort. - throw new MWException( "Problem running requested parser hook from the test file" ); - } - - $this->test = [ - 'test' => ParserTest::chomp( $this->sectionData['test'] ), - 'subtest' => $this->nextSubTest, - 'input' => ParserTest::chomp( $this->sectionData[$input] ), - 'result' => ParserTest::chomp( $this->sectionData[$result] ), - 'options' => ParserTest::chomp( $this->sectionData['options'] ), - 'config' => ParserTest::chomp( $this->sectionData['config'] ), - ]; - if ( $tidy != false ) { - $this->test['options'] .= " tidy"; - } - return true; - } - - function readNextTest() { - # Run additional subtests of previous test - while ( $this->nextSubTest > 0 ) { - if ( $this->setupCurrentTest() ) { - return true; - } - } - - $this->clearSection(); - # Reset hooks for the delayed test object - $this->delayedParserTest->reset(); - - while ( false !== ( $line = fgets( $this->fh ) ) ) { - $this->lineNum++; - $matches = []; - - if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) { - $this->section = strtolower( $matches[1] ); - - if ( $this->section == 'endarticle' ) { - $this->checkSection( 'text' ); - $this->checkSection( 'article' ); - - $this->parserTest->addArticle( - ParserTest::chomp( $this->sectionData['article'] ), - $this->sectionData['text'], $this->lineNum ); - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endhooks' ) { - $this->checkSection( 'hooks' ); - - foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endfunctionhooks' ) { - $this->checkSection( 'functionhooks' ); - - foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireFunctionHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endtransparenthooks' ) { - $this->checkSection( 'transparenthooks' ); - - foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireTransparentHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'end' ) { - $this->checkSection( 'test' ); - do { - if ( $this->setupCurrentTest() ) { - return true; - } - } while ( $this->nextSubTest > 0 ); - # go on to next test (since this was disabled) - $this->clearSection(); - $this->delayedParserTest->reset(); - continue; - } - - if ( isset( $this->sectionData[$this->section] ) ) { - throw new MWException( "duplicate section '$this->section' " - . "at line {$this->lineNum} of $this->file\n" ); - } - - $this->sectionData[$this->section] = ''; - - continue; - } - - if ( $this->section ) { - $this->sectionData[$this->section] .= $line; - } - } - - return false; - } - - /** - * Clear section name and its data - */ - private function clearSection() { - $this->sectionData = []; - $this->section = null; - - } - - /** - * Verify the current section data has some value for the given token - * name(s) (first parameter). - * Throw an exception if it is not set, referencing current section - * and adding the current file name and line number - * - * @param string|array $tokens Expected token(s) that should have been - * mentioned before closing this section - * @param bool $fatal True iff an exception should be thrown if - * the section is not found. - * @return bool|string - * @throws MWException - */ - private function checkSection( $tokens, $fatal = true ) { - if ( is_null( $this->section ) ) { - throw new MWException( __METHOD__ . " can not verify a null section!\n" ); - } - if ( !is_array( $tokens ) ) { - $tokens = [ $tokens ]; - } - if ( count( $tokens ) == 0 ) { - throw new MWException( __METHOD__ . " can not verify zero sections!\n" ); - } - - $data = $this->sectionData; - $tokens = array_filter( $tokens, function ( $token ) use ( $data ) { - return isset( $data[$token] ); - } ); - - if ( count( $tokens ) == 0 ) { - if ( !$fatal ) { - return false; - } - throw new MWException( sprintf( - "'%s' without '%s' at line %s of %s\n", - $this->section, - implode( ',', $tokens ), - $this->lineNum, - $this->file - ) ); - } - if ( count( $tokens ) > 1 ) { - throw new MWException( sprintf( - "'%s' with unexpected tokens '%s' at line %s of %s\n", - $this->section, - implode( ',', $tokens ), - $this->lineNum, - $this->file - ) ); - } - - return array_values( $tokens )[0]; - } -} - diff --git a/tests/parser/TestFileReader.php b/tests/parser/TestFileReader.php new file mode 100644 index 0000000000..18e09ccb7f --- /dev/null +++ b/tests/parser/TestFileReader.php @@ -0,0 +1,324 @@ +file = $file; + $this->fh = fopen( $this->file, "rt" ); + + if ( !$this->fh ) { + throw new MWException( "Couldn't open file '$file'\n" ); + } + + $this->parserTest = $parserTest; + $this->delayedParserTest = new DelayedParserTest(); + + $this->lineNum = $this->index = 0; + } + + function rewind() { + if ( fseek( $this->fh, 0 ) ) { + throw new MWException( "Couldn't fseek to the start of '$this->file'\n" ); + } + + $this->index = -1; + $this->lineNum = 0; + $this->eof = false; + $this->next(); + + return true; + } + + function current() { + return $this->test; + } + + function key() { + return $this->index; + } + + function next() { + if ( $this->readNextTest() ) { + $this->index++; + return true; + } else { + $this->eof = true; + } + } + + function valid() { + return $this->eof != true; + } + + function setupCurrentTest() { + // "input" and "result" are old section names allowed + // for backwards-compatibility. + $input = $this->checkSection( [ 'wikitext', 'input' ], false ); + $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false ); + // some tests have "with tidy" and "without tidy" variants + $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false ); + if ( $tidy != false ) { + if ( $this->nextSubTest == 0 ) { + if ( $result != false ) { + $this->nextSubTest = 1; // rerun non-tidy variant later + } + $result = $tidy; + } else { + $this->nextSubTest = 0; // go on to next test after this + $tidy = false; + } + } + + if ( !isset( $this->sectionData['options'] ) ) { + $this->sectionData['options'] = ''; + } + + if ( !isset( $this->sectionData['config'] ) ) { + $this->sectionData['config'] = ''; + } + + $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && + !$this->parserTest->runDisabled; + $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && + $result == 'html' && + !$this->parserTest->runParsoid; + $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] ); + if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) { + # disabled test + return false; + } + + # We are really going to run the test, run pending hooks and hooks function + wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" ); + $hooksResult = $this->delayedParserTest->unleash( $this->parserTest ); + if ( !$hooksResult ) { + # Some hook reported an issue. Abort. + throw new MWException( "Problem running requested parser hook from the test file" ); + } + + $this->test = [ + 'test' => ParserTestRunner::chomp( $this->sectionData['test'] ), + 'subtest' => $this->nextSubTest, + 'input' => ParserTestRunner::chomp( $this->sectionData[$input] ), + 'result' => ParserTestRunner::chomp( $this->sectionData[$result] ), + 'options' => ParserTestRunner::chomp( $this->sectionData['options'] ), + 'config' => ParserTestRunner::chomp( $this->sectionData['config'] ), + ]; + if ( $tidy != false ) { + $this->test['options'] .= " tidy"; + } + return true; + } + + function readNextTest() { + # Run additional subtests of previous test + while ( $this->nextSubTest > 0 ) { + if ( $this->setupCurrentTest() ) { + return true; + } + } + + $this->clearSection(); + # Reset hooks for the delayed test object + $this->delayedParserTest->reset(); + + while ( false !== ( $line = fgets( $this->fh ) ) ) { + $this->lineNum++; + $matches = []; + + if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) { + $this->section = strtolower( $matches[1] ); + + if ( $this->section == 'endarticle' ) { + $this->checkSection( 'text' ); + $this->checkSection( 'article' ); + + $this->parserTest->addArticle( + ParserTestRunner::chomp( $this->sectionData['article'] ), + $this->sectionData['text'], $this->lineNum ); + + $this->clearSection(); + + continue; + } + + if ( $this->section == 'endhooks' ) { + $this->checkSection( 'hooks' ); + + foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) { + $line = trim( $line ); + + if ( $line ) { + $this->delayedParserTest->requireHook( $line ); + } + } + + $this->clearSection(); + + continue; + } + + if ( $this->section == 'endfunctionhooks' ) { + $this->checkSection( 'functionhooks' ); + + foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) { + $line = trim( $line ); + + if ( $line ) { + $this->delayedParserTest->requireFunctionHook( $line ); + } + } + + $this->clearSection(); + + continue; + } + + if ( $this->section == 'endtransparenthooks' ) { + $this->checkSection( 'transparenthooks' ); + + foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) { + $line = trim( $line ); + + if ( $line ) { + $this->delayedParserTest->requireTransparentHook( $line ); + } + } + + $this->clearSection(); + + continue; + } + + if ( $this->section == 'end' ) { + $this->checkSection( 'test' ); + do { + if ( $this->setupCurrentTest() ) { + return true; + } + } while ( $this->nextSubTest > 0 ); + # go on to next test (since this was disabled) + $this->clearSection(); + $this->delayedParserTest->reset(); + continue; + } + + if ( isset( $this->sectionData[$this->section] ) ) { + throw new MWException( "duplicate section '$this->section' " + . "at line {$this->lineNum} of $this->file\n" ); + } + + $this->sectionData[$this->section] = ''; + + continue; + } + + if ( $this->section ) { + $this->sectionData[$this->section] .= $line; + } + } + + return false; + } + + /** + * Clear section name and its data + */ + private function clearSection() { + $this->sectionData = []; + $this->section = null; + + } + + /** + * Verify the current section data has some value for the given token + * name(s) (first parameter). + * Throw an exception if it is not set, referencing current section + * and adding the current file name and line number + * + * @param string|array $tokens Expected token(s) that should have been + * mentioned before closing this section + * @param bool $fatal True iff an exception should be thrown if + * the section is not found. + * @return bool|string + * @throws MWException + */ + private function checkSection( $tokens, $fatal = true ) { + if ( is_null( $this->section ) ) { + throw new MWException( __METHOD__ . " can not verify a null section!\n" ); + } + if ( !is_array( $tokens ) ) { + $tokens = [ $tokens ]; + } + if ( count( $tokens ) == 0 ) { + throw new MWException( __METHOD__ . " can not verify zero sections!\n" ); + } + + $data = $this->sectionData; + $tokens = array_filter( $tokens, function ( $token ) use ( $data ) { + return isset( $data[$token] ); + } ); + + if ( count( $tokens ) == 0 ) { + if ( !$fatal ) { + return false; + } + throw new MWException( sprintf( + "'%s' without '%s' at line %s of %s\n", + $this->section, + implode( ',', $tokens ), + $this->lineNum, + $this->file + ) ); + } + if ( count( $tokens ) > 1 ) { + throw new MWException( sprintf( + "'%s' with unexpected tokens '%s' at line %s of %s\n", + $this->section, + implode( ',', $tokens ), + $this->lineNum, + $this->file + ) ); + } + + return array_values( $tokens )[0]; + } +} + diff --git a/tests/parser/fuzzTest.php b/tests/parser/fuzzTest.php index 045a7700cf..ddf839eeec 100644 --- a/tests/parser/fuzzTest.php +++ b/tests/parser/fuzzTest.php @@ -22,13 +22,13 @@ class ParserFuzzTest extends Maintenance { } function finalSetup() { - require_once __DIR__ . '/../TestsAutoLoader.php'; + require_once __DIR__ . '/../common/TestsAutoLoader.php'; } function execute() { $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] ); $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1; - $this->parserTest = new ParserTest; + $this->parserTest = new ParserTestRunner; $this->fuzzTest( $files ); } diff --git a/tests/parser/parserTests.php b/tests/parser/parserTests.php new file mode 100644 index 0000000000..48c260678e --- /dev/null +++ b/tests/parser/parserTests.php @@ -0,0 +1,96 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Testing + */ + +define( 'MW_PARSER_TEST', true ); + +$options = [ 'quick', 'color', 'quiet', 'help', 'show-output', + 'record', 'run-disabled', 'run-parsoid', 'dwdiff', 'mark-ws' ]; +$optionsWithArgs = [ 'regex', 'filter', 'seed', 'setversion', 'file', 'norm' ]; + +require_once __DIR__ . '/../../maintenance/commandLine.inc'; +require_once __DIR__ . '/../common/TestsAutoLoader.php'; + +if ( isset( $options['help'] ) ) { + echo << Run test cases from a custom file instead of parserTests.txt + --record Record tests in database + --compare Compare with recorded results, without updating the database. + --setversion When using --record, set the version string to use (useful + with git-svn so that you can get the exact revision) + --keep-uploads Re-use the same upload directory for each test, don't delete it + --run-disabled run disabled tests + --run-parsoid run parsoid tests (normally disabled) + --dwdiff Use dwdiff to display diff output + --mark-ws Mark whitespace in diffs by replacing it with symbols + --norm= Apply a comma-separated list of normalization functions to + both the expected and actual output in order to resolve + irrelevant differences. The accepted normalization functions + are: removeTbody to remove tags; and trimWhitespace + to trim whitespace from the start and end of text nodes. + --use-tidy-config Use the wiki's Tidy configuration instead of known-good + defaults. + --help Show this help message + +ENDS; + exit( 0 ); +} + +# Cases of weird db corruption were encountered when running tests on earlyish +# versions of SQLite +if ( $wgDBtype == 'sqlite' ) { + $db = wfGetDB( DB_MASTER ); + $version = $db->getServerVersion(); + if ( version_compare( $version, '3.6' ) < 0 ) { + die( "Parser tests require SQLite version 3.6 or later, you have $version\n" ); + } +} + +$tester = new ParserTestRunner( $options ); + +if ( isset( $options['file'] ) ) { + $files = [ $options['file'] ]; +} else { + // Default parser tests and any set from extensions or local config + $files = $wgParserTestFiles; +} + +# Print out software version to assist with locating regressions +$version = SpecialVersion::getVersion( 'nodb' ); +echo "This is MediaWiki version {$version}.\n\n"; + +$ok = $tester->runTestsFromFiles( $files ); +exit( $ok ? 0 : 1 ); diff --git a/tests/parserTests.php b/tests/parserTests.php deleted file mode 100644 index 915eac63a7..0000000000 --- a/tests/parserTests.php +++ /dev/null @@ -1,96 +0,0 @@ - - * https://www.mediawiki.org/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Testing - */ - -define( 'MW_PARSER_TEST', true ); - -$options = [ 'quick', 'color', 'quiet', 'help', 'show-output', - 'record', 'run-disabled', 'run-parsoid', 'dwdiff', 'mark-ws' ]; -$optionsWithArgs = [ 'regex', 'filter', 'seed', 'setversion', 'file', 'norm' ]; - -require_once __DIR__ . '/../maintenance/commandLine.inc'; -require_once __DIR__ . '/TestsAutoLoader.php'; - -if ( isset( $options['help'] ) ) { - echo << Run test cases from a custom file instead of parserTests.txt - --record Record tests in database - --compare Compare with recorded results, without updating the database. - --setversion When using --record, set the version string to use (useful - with git-svn so that you can get the exact revision) - --keep-uploads Re-use the same upload directory for each test, don't delete it - --run-disabled run disabled tests - --run-parsoid run parsoid tests (normally disabled) - --dwdiff Use dwdiff to display diff output - --mark-ws Mark whitespace in diffs by replacing it with symbols - --norm= Apply a comma-separated list of normalization functions to - both the expected and actual output in order to resolve - irrelevant differences. The accepted normalization functions - are: removeTbody to remove tags; and trimWhitespace - to trim whitespace from the start and end of text nodes. - --use-tidy-config Use the wiki's Tidy configuration instead of known-good - defaults. - --help Show this help message - -ENDS; - exit( 0 ); -} - -# Cases of weird db corruption were encountered when running tests on earlyish -# versions of SQLite -if ( $wgDBtype == 'sqlite' ) { - $db = wfGetDB( DB_MASTER ); - $version = $db->getServerVersion(); - if ( version_compare( $version, '3.6' ) < 0 ) { - die( "Parser tests require SQLite version 3.6 or later, you have $version\n" ); - } -} - -$tester = new ParserTest( $options ); - -if ( isset( $options['file'] ) ) { - $files = [ $options['file'] ]; -} else { - // Default parser tests and any set from extensions or local config - $files = $wgParserTestFiles; -} - -# Print out software version to assist with locating regressions -$version = SpecialVersion::getVersion( 'nodb' ); -echo "This is MediaWiki version {$version}.\n\n"; - -$ok = $tester->runTestsFromFiles( $files ); -exit( $ok ? 0 : 1 ); diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php deleted file mode 100644 index 173447fcf7..0000000000 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ /dev/null @@ -1,138 +0,0 @@ - "\\'", '\\' => '\\\\' ] ); - $parserTestClassName = ucfirst( $testsName ); - - // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php - // Prepend 'ParserTest_' to be paranoid about it not starting with a number - $parserTestClassName = 'ParserTest_' . - preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName ); - - if ( isset( $testList[$parserTestClassName] ) ) { - // If a conflict happens, gives a very unclear fatal. - // So as a last ditch effort to prevent that eventuality, if there - // is a conflict, append a number. - $counter++; - $parserTestClassName .= $counter; - } - $testList[$parserTestClassName] = true; - $parserTestClassDefinition = <<addTestSuite( $parserTestClassName ); - } - return $suite; - } - - /** - * Write $msg under log group 'tests-parser' - * @param string $msg Message to log - */ - protected static function debug( $msg ) { - return wfDebugLog( 'tests-parser', wfGetCaller() . ' ' . $msg ); - } -} diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php deleted file mode 100644 index 097e4136c3..0000000000 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ /dev/null @@ -1,995 +0,0 @@ -getCliArg( 'regex' ) ) { - $this->regex = $this->getCliArg( 'regex' ); - } else { - # Matches anything - $this->regex = ''; - } - - $this->keepUploads = $this->getCliArg( 'keep-uploads' ); - - $tmpGlobals = []; - - $tmpGlobals['wgLanguageCode'] = 'en'; - $tmpGlobals['wgContLang'] = Language::factory( 'en' ); - $tmpGlobals['wgSitename'] = 'MediaWiki'; - $tmpGlobals['wgServer'] = 'http://example.org'; - $tmpGlobals['wgServerName'] = 'example.org'; - $tmpGlobals['wgScriptPath'] = ''; - $tmpGlobals['wgScript'] = '/index.php'; - $tmpGlobals['wgResourceBasePath'] = ''; - $tmpGlobals['wgStylePath'] = '/skins'; - $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; - $tmpGlobals['wgArticlePath'] = '/wiki/$1'; - $tmpGlobals['wgActionPaths'] = []; - $tmpGlobals['wgVariantArticlePath'] = false; - $tmpGlobals['wgEnableUploads'] = true; - $tmpGlobals['wgUploadNavigationUrl'] = false; - $tmpGlobals['wgThumbnailScriptPath'] = false; - $tmpGlobals['wgLocalFileRepo'] = [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => 'local-backend' - ]; - $tmpGlobals['wgForeignFileRepos'] = []; - $tmpGlobals['wgDefaultExternalStore'] = []; - $tmpGlobals['wgParserCacheType'] = CACHE_NONE; - $tmpGlobals['wgCapitalLinks'] = true; - $tmpGlobals['wgNoFollowLinks'] = true; - $tmpGlobals['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ]; - $tmpGlobals['wgExternalLinkTarget'] = false; - $tmpGlobals['wgThumbnailScriptPath'] = false; - $tmpGlobals['wgUseImageResize'] = true; - $tmpGlobals['wgAllowExternalImages'] = true; - $tmpGlobals['wgRawHtml'] = false; - $tmpGlobals['wgExperimentalHtmlIds'] = false; - $tmpGlobals['wgAdaptiveMessageCache'] = true; - $tmpGlobals['wgUseDatabaseMessages'] = true; - $tmpGlobals['wgLocaltimezone'] = 'UTC'; - $tmpGlobals['wgGroupPermissions'] = [ - '*' => [ - 'createaccount' => true, - 'read' => true, - 'edit' => true, - 'createpage' => true, - 'createtalk' => true, - ] ]; - $tmpGlobals['wgNamespaceProtection'] = [ NS_MEDIAWIKI => 'editinterface' ]; - - $tmpGlobals['wgParser'] = new StubObject( - 'wgParser', $GLOBALS['wgParserConf']['class'], - [ $GLOBALS['wgParserConf'] ] ); - - $tmpGlobals['wgFileExtensions'][] = 'svg'; - $tmpGlobals['wgSVGConverter'] = 'rsvg'; - $tmpGlobals['wgSVGConverters']['rsvg'] = - '$path/rsvg-convert -w $width -h $height -o $output $input'; - - if ( $GLOBALS['wgStyleDirectory'] === false ) { - $tmpGlobals['wgStyleDirectory'] = "$IP/skins"; - } - - $tmpHooks = $wgHooks; - $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; - $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; - $tmpGlobals['wgHooks'] = $tmpHooks; - # add a namespace shadowing a interwiki link, to test - # proper precedence when resolving links. (bug 51680) - $tmpGlobals['wgExtraNamespaces'] = [ - 100 => 'MemoryAlpha', - 101 => 'MemoryAlpha_talk' - ]; - - $tmpGlobals['wgLocalInterwikis'] = [ 'local', 'mi' ]; - # "extra language links" - # see https://gerrit.wikimedia.org/r/111390 - $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = [ 'mul' ]; - - // DjVu support - $this->djVuSupport = new DjVuSupport(); - // Tidy support - $this->tidySupport = new TidySupport(); - $tmpGlobals['wgTidyConfig'] = $this->tidySupport->getConfig(); - $tmpGlobals['wgUseTidy'] = false; - - $this->setMwGlobals( $tmpGlobals ); - - $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image']; - $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk']; - - $wgNamespaceAliases['Image'] = NS_FILE; - $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; - - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache - $wgContLang->resetNamespaces(); # reset namespace cache - ParserTest::resetTitleServices(); - MediaWikiServices::getInstance()->disableService( 'MediaHandlerFactory' ); - MediaWikiServices::getInstance()->redefineService( - 'MediaHandlerFactory', - function() { - return new MockMediaHandlerFactory(); - } - ); - } - - protected function tearDown() { - global $wgNamespaceAliases, $wgContLang; - - $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias']; - $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias']; - - MWTidy::destroySingleton(); - - // Restore backends - RepoGroup::destroySingleton(); - FileBackendGroup::destroySingleton(); - - // Remove temporary pages from the link cache - LinkCache::singleton()->clear(); - - // Restore message cache (temporary pages and $wgUseDatabaseMessages) - MessageCache::destroyInstance(); - MediaWikiServices::getInstance()->resetServiceForTesting( 'MediaHandlerFactory' ); - - parent::tearDown(); - - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache - $wgContLang->resetNamespaces(); # reset namespace cache - } - - public static function tearDownAfterClass() { - ParserTest::tearDownInterwikis(); - parent::tearDownAfterClass(); - } - - function addDBDataOnce() { - # disabled for performance - # $this->tablesUsed[] = 'image'; - - # Update certain things in site_stats - $this->db->insert( 'site_stats', - [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ], - __METHOD__, - [ 'IGNORE' ] - ); - - $user = User::newFromId( 0 ); - LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision - - # Upload DB table entries for files. - # We will upload the actual files later. Note that if anything causes LocalFile::load() - # to be triggered before then, it will break via maybeUpgrade() setting the fileExists - # member to false and storing it in cache. - # note that the size/width/height/bits/etc of the file - # are actually set by inspecting the file itself; the arguments - # to recordUpload2 have no effect. That said, we try to make things - # match up so it is less confusing to readers of the code & tests. - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'Upload of some lame file', - 'Some lame file', - [ - 'size' => 7881, - 'width' => 1941, - 'height' => 220, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20010115123500' ), $user - ); - } - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'Upload of some lame thumbnail', - 'Some lame thumbnail', - [ - 'size' => 22589, - 'width' => 135, - 'height' => 135, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/png', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20130225203040' ), $user - ); - } - - # This image will be blacklisted in [[MediaWiki:Bad image list]] - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'zomgnotcensored', - 'Borderline image', - [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 24, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20010115123500' ), $user - ); - } - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ - 'size' => 12345, - 'width' => 240, - 'height' => 180, - 'bits' => 0, - 'media_type' => MEDIATYPE_DRAWING, - 'mime' => 'image/svg+xml', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - } - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 0, - 'media_type' => MEDIATYPE_VIDEO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 32 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - } - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Audio.oga' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'An awesome hitsong ', 'Will it play', [ - 'size' => 12345, - 'width' => 0, - 'height' => 0, - 'bits' => 0, - 'media_type' => MEDIATYPE_AUDIO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 32 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - } - - # A DjVu file - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ - 'size' => 3249, - 'width' => 2480, - 'height' => 3508, - 'bits' => 0, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/vnd.djvu', - 'metadata' => ' - - - - - - - - - - - - - - - - - - - - - - - - -', - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20140115123600' ), $user ); - } - } - - // ParserTest setup/teardown functions - - /** - * Set up the global variables for a consistent environment for each test. - * Ideally this should replace the global configuration entirely. - * @param array $opts - * @param string $config - * @return RequestContext - */ - protected function setupGlobals( $opts = [], $config = '' ) { - global $wgFileBackends; - # Find out values for some special options. - $lang = - self::getOptionValue( 'language', $opts, 'en' ); - $variant = - self::getOptionValue( 'variant', $opts, false ); - $maxtoclevel = - self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); - $linkHolderBatchSize = - self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); - - $uploadDir = $this->getUploadDir(); - if ( $this->getCliArg( 'use-filebackend' ) ) { - if ( self::$backendToUse ) { - $backend = self::$backendToUse; - } else { - $name = $this->getCliArg( 'use-filebackend' ); - $useConfig = []; - foreach ( $wgFileBackends as $conf ) { - if ( $conf['name'] == $name ) { - $useConfig = $conf; - } - } - $useConfig['name'] = 'local-backend'; // swap name - unset( $useConfig['lockManager'] ); - unset( $useConfig['fileJournal'] ); - $class = $useConfig['class']; - self::$backendToUse = new $class( $useConfig ); - $backend = self::$backendToUse; - } - } else { - # Replace with a mock. We do not care about generating real - # files on the filesystem, just need to expose the file - # informations. - $backend = new MockFileBackend( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID() - ] ); - } - - $settings = [ - 'wgLocalFileRepo' => [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => $backend - ], - 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), - 'wgLanguageCode' => $lang, - 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_', - 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), - 'wgNamespacesWithSubpages' => [ NS_MAIN => isset( $opts['subpage'] ) ], - 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), - 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], - 'wgMaxTocLevel' => $maxtoclevel, - 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ), - 'wgMathDirectory' => $uploadDir . '/math', - 'wgDefaultLanguageVariant' => $variant, - 'wgLinkHolderBatchSize' => $linkHolderBatchSize, - 'wgUseTidy' => false, - 'wgTidyConfig' => isset( $opts['tidy'] ) ? $this->tidySupport->getConfig() : null - ]; - - if ( $config ) { - $configLines = explode( "\n", $config ); - - foreach ( $configLines as $line ) { - list( $var, $value ) = explode( '=', $line, 2 ); - - $settings[$var] = eval( "return $value;" ); // ??? - } - } - - $this->savedGlobals = []; - - /** @since 1.20 */ - Hooks::run( 'ParserTestGlobals', [ &$settings ] ); - - $langObj = Language::factory( $lang ); - $settings['wgContLang'] = $langObj; - $settings['wgLang'] = $langObj; - - $context = new RequestContext(); - $settings['wgOut'] = $context->getOutput(); - $settings['wgUser'] = $context->getUser(); - $settings['wgRequest'] = $context->getRequest(); - - // We (re)set $wgThumbLimits to a single-element array above. - $context->getUser()->setOption( 'thumbsize', 0 ); - - foreach ( $settings as $var => $val ) { - if ( array_key_exists( $var, $GLOBALS ) ) { - $this->savedGlobals[$var] = $GLOBALS[$var]; - } - - $GLOBALS[$var] = $val; - } - - MWTidy::destroySingleton(); - MagicWord::clearCache(); - - # The entries saved into RepoGroup cache with previous globals will be wrong. - RepoGroup::destroySingleton(); - FileBackendGroup::destroySingleton(); - - # Create dummy files in storage - $this->setupUploads(); - - # Publish the articles after we have the final language set - $this->publishTestArticles(); - - MessageCache::destroyInstance(); - - return $context; - } - - /** - * Get an FS upload directory (only applies to FSFileBackend) - * - * @return string The directory - */ - protected function getUploadDir() { - if ( $this->keepUploads ) { - // Don't use getNewTempDirectory() as this is meant to persist - $dir = wfTempDir() . '/mwParser-images'; - - if ( is_dir( $dir ) ) { - return $dir; - } - } else { - $dir = $this->getNewTempDirectory(); - } - - if ( file_exists( $dir ) ) { - wfDebug( "Already exists!\n" ); - - return $dir; - } - - return $dir; - } - - /** - * Create a dummy uploads directory which will contain a couple - * of files in order to pass existence tests. - * - * @return string The directory - */ - protected function setupUploads() { - global $IP; - - $base = $this->getBaseDir(); - $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); - $backend->prepare( [ 'dir' => "$base/local-public/3/3a" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", - 'dst' => "$base/local-public/3/3a/Foobar.jpg" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/e/ea" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/wiki.png", - 'dst' => "$base/local-public/e/ea/Thumb.png" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/0/09" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", - 'dst' => "$base/local-public/0/09/Bad.jpg" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/5/5f" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", - 'dst' => "$base/local-public/5/5f/LoremIpsum.djvu" - ] ); - - // No helpful SVG file to copy, so make one ourselves - $data = '' . - ''; - - $backend->prepare( [ 'dir' => "$base/local-public/f/ff" ] ); - $backend->quickCreate( [ - 'content' => $data, 'dst' => "$base/local-public/f/ff/Foobar.svg" - ] ); - } - - /** - * Restore default values and perform any necessary clean-up - * after each test runs. - */ - protected function teardownGlobals() { - $this->teardownUploads(); - - foreach ( $this->savedGlobals as $var => $val ) { - $GLOBALS[$var] = $val; - } - } - - /** - * Remove the dummy uploads directory - */ - private function teardownUploads() { - if ( $this->keepUploads ) { - return; - } - - $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); - if ( $backend instanceof MockFileBackend ) { - # In memory backend, so dont bother cleaning them up. - return; - } - - $base = $this->getBaseDir(); - // delete the files first, then the dirs. - self::deleteFiles( - [ - "$base/local-public/3/3a/Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/100px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/137px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/177px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/206px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/220px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/265px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/274px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/330px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/353px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/440px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/442px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/450px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/50px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/600px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/75px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg", - - "$base/local-public/e/ea/Thumb.png", - - "$base/local-public/0/09/Bad.jpg", - - "$base/local-public/5/5f/LoremIpsum.djvu", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg", - - "$base/local-public/f/ff/Foobar.svg", - "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png", - - "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", - ] - ); - } - - /** - * Delete the specified files, if they exist. - * @param array $files Full paths to files to delete. - */ - private static function deleteFiles( $files ) { - $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); - foreach ( $files as $file ) { - $backend->delete( [ 'src' => $file ], [ 'force' => 1 ] ); - } - foreach ( $files as $file ) { - $tmp = FileBackend::parentStoragePath( $file ); - while ( $tmp ) { - if ( !$backend->clean( [ 'dir' => $tmp ] )->isOK() ) { - break; - } - $tmp = FileBackend::parentStoragePath( $tmp ); - } - } - } - - protected function getBaseDir() { - return 'mwstore://local-backend'; - } - - public function parserTestProvider() { - if ( $this->file === false ) { - global $wgParserTestFiles; - $this->file = $wgParserTestFiles[0]; - } - - return new TestFileDataProvider( $this->file, $this ); - } - - /** - * Set the file from whose tests will be run by this instance - * @param string $filename - */ - public function setParserTestFile( $filename ) { - $this->file = $filename; - } - - /** - * @group medium - * @group ParserTests - * @dataProvider parserTestProvider - * @param string $desc - * @param string $input - * @param string $result - * @param array $opts - * @param array $config - */ - public function testParserTest( $desc, $input, $result, $opts, $config ) { - if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) { - $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions" - // $this->markTestSkipped( 'Filtered out by the user' ); - $this->teardownGlobals(); - return; - } - - if ( !$this->isWikitextNS( NS_MAIN ) ) { - // parser tests frequently assume that the main namespace contains wikitext. - // @todo When setting up pages, force the content model. Only skip if - // $wgtContentModelUseDB is false. - $this->teardownGlobals(); - $this->markTestSkipped( "Main namespace does not support wikitext," - . "skipping parser test: $desc" ); - } - - wfDebug( "Running parser test: $desc\n" ); - - $opts = $this->parseOptions( $opts ); - $context = $this->setupGlobals( $opts, $config ); - - $user = $context->getUser(); - $options = ParserOptions::newFromContext( $context ); - - if ( isset( $opts['title'] ) ) { - $titleText = $opts['title']; - } else { - $titleText = 'Parser test'; - } - - $local = isset( $opts['local'] ); - $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; - $parser = $this->getParser( $preprocessor ); - - $title = Title::newFromText( $titleText ); - - # Parser test requiring math. Make sure texvc is executable - # or just skip such tests. - if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) { - global $wgTexvc; - - if ( !isset( $wgTexvc ) ) { - $this->teardownGlobals(); - $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" ); - } elseif ( !is_executable( $wgTexvc ) ) { - $this->teardownGlobals(); - $this->markTestSkipped( "SKIPPED: texvc binary does not exist" - . " or is not executable.\n" - . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" ); - } - } - - if ( isset( $opts['djvu'] ) ) { - if ( !$this->djVuSupport->isEnabled() ) { - $this->teardownGlobals(); - $this->markTestSkipped( "SKIPPED: djvu binaries do not exist or are not executable.\n" ); - } - } - - if ( isset( $opts['tidy'] ) ) { - if ( !$this->tidySupport->isEnabled() ) { - $this->teardownGlobals(); - $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" ); - } else { - $options->setTidy( true ); - } - } - - if ( isset( $opts['pst'] ) ) { - $out = $parser->preSaveTransform( $input, $title, $user, $options ); - } elseif ( isset( $opts['msg'] ) ) { - $out = $parser->transformMsg( $input, $options, $title ); - } elseif ( isset( $opts['section'] ) ) { - $section = $opts['section']; - $out = $parser->getSection( $input, $section ); - } elseif ( isset( $opts['replace'] ) ) { - $section = $opts['replace'][0]; - $replace = $opts['replace'][1]; - $out = $parser->replaceSection( $input, $section, $replace ); - } elseif ( isset( $opts['comment'] ) ) { - $out = Linker::formatComment( $input, $title, $local ); - } elseif ( isset( $opts['preload'] ) ) { - $out = $parser->getPreloadText( $input, $title, $options ); - } else { - $output = $parser->parse( $input, $title, $options, true, true, 1337 ); - $output->setTOCEnabled( !isset( $opts['notoc'] ) ); - $out = $output->getText(); - if ( isset( $opts['tidy'] ) ) { - $out = preg_replace( '/\s+$/', '', $out ); - } - - if ( isset( $opts['showtitle'] ) ) { - if ( $output->getTitleText() ) { - $title = $output->getTitleText(); - } - - $out = "$title\n$out"; - } - - if ( isset( $opts['showindicators'] ) ) { - $indicators = ''; - foreach ( $output->getIndicators() as $id => $content ) { - $indicators .= "$id=$content\n"; - } - $out = $indicators . $out; - } - - if ( isset( $opts['ill'] ) ) { - $out = implode( ' ', $output->getLanguageLinks() ); - } elseif ( isset( $opts['cat'] ) ) { - $outputPage = $context->getOutput(); - $outputPage->addCategoryLinks( $output->getCategories() ); - $cats = $outputPage->getCategoryLinks(); - - if ( isset( $cats['normal'] ) ) { - $out = implode( ' ', $cats['normal'] ); - } else { - $out = ''; - } - } - $parser->mPreprocessor = null; - } - - $this->teardownGlobals(); - - $this->assertEquals( $result, $out, $desc ); - } - - /** - * Get a Parser object - * @param Preprocessor $preprocessor - * @return Parser - */ - function getParser( $preprocessor = null ) { - global $wgParserConf; - - $class = $wgParserConf['class']; - $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); - - Hooks::run( 'ParserTestParser', [ &$parser ] ); - - return $parser; - } - - // Various action functions - - public function addArticle( $name, $text, $line ) { - self::$articles[$name] = [ $text, $line ]; - } - - public function publishTestArticles() { - if ( empty( self::$articles ) ) { - return; - } - - foreach ( self::$articles as $name => $info ) { - list( $text, $line ) = $info; - ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' ); - } - } - - /** - * Steal a callback function from the primary parser, save it for - * application to our scary parser. If the hook is not installed, - * abort processing of this file. - * - * @param string $name - * @return bool True if tag hook is present - */ - public function requireHook( $name ) { - global $wgParser; - $wgParser->firstCallInit(); // make sure hooks are loaded. - return isset( $wgParser->mTagHooks[$name] ); - } - - public function requireFunctionHook( $name ) { - global $wgParser; - $wgParser->firstCallInit(); // make sure hooks are loaded. - return isset( $wgParser->mFunctionHooks[$name] ); - } - - public function requireTransparentHook( $name ) { - global $wgParser; - $wgParser->firstCallInit(); // make sure hooks are loaded. - return isset( $wgParser->mTransparentTagHooks[$name] ); - } - - // Various "cleanup" functions - - /** - * Remove last character if it is a newline - * @param string $s - * @return string - */ - public function removeEndingNewline( $s ) { - if ( substr( $s, -1 ) === "\n" ) { - return substr( $s, 0, -1 ); - } else { - return $s; - } - } - - // Test options parser functions - - protected function parseOptions( $instring ) { - $opts = []; - // foo - // foo=bar - // foo="bar baz" - // foo=[[bar baz]] - // foo=bar,"baz quux" - $regex = '/\b - ([\w-]+) # Key - \b - (?:\s* - = # First sub-value - \s* - ( - " - [^"]* # Quoted val - " - | - \[\[ - [^]]* # Link target - \]\] - | - [\w-]+ # Plain word - ) - (?:\s* - , # Sub-vals 1..N - \s* - ( - "[^"]*" # Quoted val - | - \[\[[^]]*\]\] # Link target - | - [\w-]+ # Plain word - ) - )* - )? - /x'; - - if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { - foreach ( $matches as $bits ) { - array_shift( $bits ); - $key = strtolower( array_shift( $bits ) ); - if ( count( $bits ) == 0 ) { - $opts[$key] = true; - } elseif ( count( $bits ) == 1 ) { - $opts[$key] = $this->cleanupOption( array_shift( $bits ) ); - } else { - // Array! - $opts[$key] = array_map( [ $this, 'cleanupOption' ], $bits ); - } - } - } - - return $opts; - } - - protected function cleanupOption( $opt ) { - if ( substr( $opt, 0, 1 ) == '"' ) { - return substr( $opt, 1, -1 ); - } - - if ( substr( $opt, 0, 2 ) == '[[' ) { - return substr( $opt, 2, -2 ); - } - - return $opt; - } - - /** - * Use a regex to find out the value of an option - * @param string $key Name of option val to retrieve - * @param array $opts Options array to look in - * @param mixed $default Default value returned if not found - * @return mixed - */ - protected static function getOptionValue( $key, $opts, $default ) { - $key = strtolower( $key ); - - if ( isset( $opts[$key] ) ) { - return $opts[$key]; - } else { - return $default; - } - } -} diff --git a/tests/phpunit/includes/parser/ParserIntegrationTest.php b/tests/phpunit/includes/parser/ParserIntegrationTest.php new file mode 100644 index 0000000000..0e219ca0c1 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserIntegrationTest.php @@ -0,0 +1,995 @@ +getCliArg( 'regex' ) ) { + $this->regex = $this->getCliArg( 'regex' ); + } else { + # Matches anything + $this->regex = ''; + } + + $this->keepUploads = $this->getCliArg( 'keep-uploads' ); + + $tmpGlobals = []; + + $tmpGlobals['wgLanguageCode'] = 'en'; + $tmpGlobals['wgContLang'] = Language::factory( 'en' ); + $tmpGlobals['wgSitename'] = 'MediaWiki'; + $tmpGlobals['wgServer'] = 'http://example.org'; + $tmpGlobals['wgServerName'] = 'example.org'; + $tmpGlobals['wgScriptPath'] = ''; + $tmpGlobals['wgScript'] = '/index.php'; + $tmpGlobals['wgResourceBasePath'] = ''; + $tmpGlobals['wgStylePath'] = '/skins'; + $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; + $tmpGlobals['wgArticlePath'] = '/wiki/$1'; + $tmpGlobals['wgActionPaths'] = []; + $tmpGlobals['wgVariantArticlePath'] = false; + $tmpGlobals['wgEnableUploads'] = true; + $tmpGlobals['wgUploadNavigationUrl'] = false; + $tmpGlobals['wgThumbnailScriptPath'] = false; + $tmpGlobals['wgLocalFileRepo'] = [ + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => 'local-backend' + ]; + $tmpGlobals['wgForeignFileRepos'] = []; + $tmpGlobals['wgDefaultExternalStore'] = []; + $tmpGlobals['wgParserCacheType'] = CACHE_NONE; + $tmpGlobals['wgCapitalLinks'] = true; + $tmpGlobals['wgNoFollowLinks'] = true; + $tmpGlobals['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ]; + $tmpGlobals['wgExternalLinkTarget'] = false; + $tmpGlobals['wgThumbnailScriptPath'] = false; + $tmpGlobals['wgUseImageResize'] = true; + $tmpGlobals['wgAllowExternalImages'] = true; + $tmpGlobals['wgRawHtml'] = false; + $tmpGlobals['wgExperimentalHtmlIds'] = false; + $tmpGlobals['wgAdaptiveMessageCache'] = true; + $tmpGlobals['wgUseDatabaseMessages'] = true; + $tmpGlobals['wgLocaltimezone'] = 'UTC'; + $tmpGlobals['wgGroupPermissions'] = [ + '*' => [ + 'createaccount' => true, + 'read' => true, + 'edit' => true, + 'createpage' => true, + 'createtalk' => true, + ] ]; + $tmpGlobals['wgNamespaceProtection'] = [ NS_MEDIAWIKI => 'editinterface' ]; + + $tmpGlobals['wgParser'] = new StubObject( + 'wgParser', $GLOBALS['wgParserConf']['class'], + [ $GLOBALS['wgParserConf'] ] ); + + $tmpGlobals['wgFileExtensions'][] = 'svg'; + $tmpGlobals['wgSVGConverter'] = 'rsvg'; + $tmpGlobals['wgSVGConverters']['rsvg'] = + '$path/rsvg-convert -w $width -h $height -o $output $input'; + + if ( $GLOBALS['wgStyleDirectory'] === false ) { + $tmpGlobals['wgStyleDirectory'] = "$IP/skins"; + } + + $tmpHooks = $wgHooks; + $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; + $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTestRunner::getFakeTimestamp'; + $tmpGlobals['wgHooks'] = $tmpHooks; + # add a namespace shadowing a interwiki link, to test + # proper precedence when resolving links. (bug 51680) + $tmpGlobals['wgExtraNamespaces'] = [ + 100 => 'MemoryAlpha', + 101 => 'MemoryAlpha_talk' + ]; + + $tmpGlobals['wgLocalInterwikis'] = [ 'local', 'mi' ]; + # "extra language links" + # see https://gerrit.wikimedia.org/r/111390 + $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = [ 'mul' ]; + + // DjVu support + $this->djVuSupport = new DjVuSupport(); + // Tidy support + $this->tidySupport = new TidySupport(); + $tmpGlobals['wgTidyConfig'] = $this->tidySupport->getConfig(); + $tmpGlobals['wgUseTidy'] = false; + + $this->setMwGlobals( $tmpGlobals ); + + $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image']; + $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk']; + + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + ParserTestRunner::resetTitleServices(); + MediaWikiServices::getInstance()->disableService( 'MediaHandlerFactory' ); + MediaWikiServices::getInstance()->redefineService( + 'MediaHandlerFactory', + function() { + return new MockMediaHandlerFactory(); + } + ); + } + + protected function tearDown() { + global $wgNamespaceAliases, $wgContLang; + + $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias']; + $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias']; + + MWTidy::destroySingleton(); + + // Restore backends + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + // Remove temporary pages from the link cache + LinkCache::singleton()->clear(); + + // Restore message cache (temporary pages and $wgUseDatabaseMessages) + MessageCache::destroyInstance(); + MediaWikiServices::getInstance()->resetServiceForTesting( 'MediaHandlerFactory' ); + + parent::tearDown(); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + public static function tearDownAfterClass() { + ParserTestRunner::tearDownInterwikis(); + parent::tearDownAfterClass(); + } + + function addDBDataOnce() { + # disabled for performance + # $this->tablesUsed[] = 'image'; + + # Update certain things in site_stats + $this->db->insert( 'site_stats', + [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ], + __METHOD__, + [ 'IGNORE' ] + ); + + $user = User::newFromId( 0 ); + LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision + + # Upload DB table entries for files. + # We will upload the actual files later. Note that if anything causes LocalFile::load() + # to be triggered before then, it will break via maybeUpgrade() setting the fileExists + # member to false and storing it in cache. + # note that the size/width/height/bits/etc of the file + # are actually set by inspecting the file itself; the arguments + # to recordUpload2 have no effect. That said, we try to make things + # match up so it is less confusing to readers of the code & tests. + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( + '', // archive name + 'Upload of some lame file', + 'Some lame file', + [ + 'size' => 7881, + 'width' => 1941, + 'height' => 220, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), + 'fileExists' => true ], + $this->db->timestamp( '20010115123500' ), $user + ); + } + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( + '', // archive name + 'Upload of some lame thumbnail', + 'Some lame thumbnail', + [ + 'size' => 22589, + 'width' => 135, + 'height' => 135, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/png', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), + 'fileExists' => true ], + $this->db->timestamp( '20130225203040' ), $user + ); + } + + # This image will be blacklisted in [[MediaWiki:Bad image list]] + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( + '', // archive name + 'zomgnotcensored', + 'Borderline image', + [ + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 24, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), + 'fileExists' => true ], + $this->db->timestamp( '20010115123500' ), $user + ); + } + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ + 'size' => 12345, + 'width' => 240, + 'height' => 180, + 'bits' => 0, + 'media_type' => MEDIATYPE_DRAWING, + 'mime' => 'image/svg+xml', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + } + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 0, + 'media_type' => MEDIATYPE_VIDEO, + 'mime' => 'application/ogg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 32 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + } + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Audio.oga' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( '', 'An awesome hitsong ', 'Will it play', [ + 'size' => 12345, + 'width' => 0, + 'height' => 0, + 'bits' => 0, + 'media_type' => MEDIATYPE_AUDIO, + 'mime' => 'application/ogg', + 'metadata' => serialize( [] ), + 'sha1' => Wikimedia\base_convert( '', 16, 36, 32 ), + 'fileExists' => true + ], $this->db->timestamp( '20010115123500' ), $user ); + } + + # A DjVu file + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); + if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { + $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ + 'size' => 3249, + 'width' => 2480, + 'height' => 3508, + 'bits' => 0, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/vnd.djvu', + 'metadata' => ' + + + + + + + + + + + + + + + + + + + + + + + + +', + 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), + 'fileExists' => true + ], $this->db->timestamp( '20140115123600' ), $user ); + } + } + + // ParserTestRunner setup/teardown functions + + /** + * Set up the global variables for a consistent environment for each test. + * Ideally this should replace the global configuration entirely. + * @param array $opts + * @param string $config + * @return RequestContext + */ + protected function setupGlobals( $opts = [], $config = '' ) { + global $wgFileBackends; + # Find out values for some special options. + $lang = + self::getOptionValue( 'language', $opts, 'en' ); + $variant = + self::getOptionValue( 'variant', $opts, false ); + $maxtoclevel = + self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); + $linkHolderBatchSize = + self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); + + $uploadDir = $this->getUploadDir(); + if ( $this->getCliArg( 'use-filebackend' ) ) { + if ( self::$backendToUse ) { + $backend = self::$backendToUse; + } else { + $name = $this->getCliArg( 'use-filebackend' ); + $useConfig = []; + foreach ( $wgFileBackends as $conf ) { + if ( $conf['name'] == $name ) { + $useConfig = $conf; + } + } + $useConfig['name'] = 'local-backend'; // swap name + unset( $useConfig['lockManager'] ); + unset( $useConfig['fileJournal'] ); + $class = $useConfig['class']; + self::$backendToUse = new $class( $useConfig ); + $backend = self::$backendToUse; + } + } else { + # Replace with a mock. We do not care about generating real + # files on the filesystem, just need to expose the file + # informations. + $backend = new MockFileBackend( [ + 'name' => 'local-backend', + 'wikiId' => wfWikiID() + ] ); + } + + $settings = [ + 'wgLocalFileRepo' => [ + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => $backend + ], + 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), + 'wgLanguageCode' => $lang, + 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_', + 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), + 'wgNamespacesWithSubpages' => [ NS_MAIN => isset( $opts['subpage'] ) ], + 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), + 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], + 'wgMaxTocLevel' => $maxtoclevel, + 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ), + 'wgMathDirectory' => $uploadDir . '/math', + 'wgDefaultLanguageVariant' => $variant, + 'wgLinkHolderBatchSize' => $linkHolderBatchSize, + 'wgUseTidy' => false, + 'wgTidyConfig' => isset( $opts['tidy'] ) ? $this->tidySupport->getConfig() : null + ]; + + if ( $config ) { + $configLines = explode( "\n", $config ); + + foreach ( $configLines as $line ) { + list( $var, $value ) = explode( '=', $line, 2 ); + + $settings[$var] = eval( "return $value;" ); // ??? + } + } + + $this->savedGlobals = []; + + /** @since 1.20 */ + Hooks::run( 'ParserTestGlobals', [ &$settings ] ); + + $langObj = Language::factory( $lang ); + $settings['wgContLang'] = $langObj; + $settings['wgLang'] = $langObj; + + $context = new RequestContext(); + $settings['wgOut'] = $context->getOutput(); + $settings['wgUser'] = $context->getUser(); + $settings['wgRequest'] = $context->getRequest(); + + // We (re)set $wgThumbLimits to a single-element array above. + $context->getUser()->setOption( 'thumbsize', 0 ); + + foreach ( $settings as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + + $GLOBALS[$var] = $val; + } + + MWTidy::destroySingleton(); + MagicWord::clearCache(); + + # The entries saved into RepoGroup cache with previous globals will be wrong. + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + # Create dummy files in storage + $this->setupUploads(); + + # Publish the articles after we have the final language set + $this->publishTestArticles(); + + MessageCache::destroyInstance(); + + return $context; + } + + /** + * Get an FS upload directory (only applies to FSFileBackend) + * + * @return string The directory + */ + protected function getUploadDir() { + if ( $this->keepUploads ) { + // Don't use getNewTempDirectory() as this is meant to persist + $dir = wfTempDir() . '/mwParser-images'; + + if ( is_dir( $dir ) ) { + return $dir; + } + } else { + $dir = $this->getNewTempDirectory(); + } + + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + + return $dir; + } + + return $dir; + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return string The directory + */ + protected function setupUploads() { + global $IP; + + $base = $this->getBaseDir(); + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + $backend->prepare( [ 'dir' => "$base/local-public/3/3a" ] ); + $backend->store( [ + 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", + 'dst' => "$base/local-public/3/3a/Foobar.jpg" + ] ); + $backend->prepare( [ 'dir' => "$base/local-public/e/ea" ] ); + $backend->store( [ + 'src' => "$IP/tests/phpunit/data/parser/wiki.png", + 'dst' => "$base/local-public/e/ea/Thumb.png" + ] ); + $backend->prepare( [ 'dir' => "$base/local-public/0/09" ] ); + $backend->store( [ + 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", + 'dst' => "$base/local-public/0/09/Bad.jpg" + ] ); + $backend->prepare( [ 'dir' => "$base/local-public/5/5f" ] ); + $backend->store( [ + 'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", + 'dst' => "$base/local-public/5/5f/LoremIpsum.djvu" + ] ); + + // No helpful SVG file to copy, so make one ourselves + $data = '' . + ''; + + $backend->prepare( [ 'dir' => "$base/local-public/f/ff" ] ); + $backend->quickCreate( [ + 'content' => $data, 'dst' => "$base/local-public/f/ff/Foobar.svg" + ] ); + } + + /** + * Restore default values and perform any necessary clean-up + * after each test runs. + */ + protected function teardownGlobals() { + $this->teardownUploads(); + + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + } + + /** + * Remove the dummy uploads directory + */ + private function teardownUploads() { + if ( $this->keepUploads ) { + return; + } + + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + if ( $backend instanceof MockFileBackend ) { + # In memory backend, so dont bother cleaning them up. + return; + } + + $base = $this->getBaseDir(); + // delete the files first, then the dirs. + self::deleteFiles( + [ + "$base/local-public/3/3a/Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/100px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/137px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/177px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/206px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/220px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/265px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/274px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/330px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/353px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/440px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/442px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/450px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/50px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/600px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/75px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg", + + "$base/local-public/e/ea/Thumb.png", + + "$base/local-public/0/09/Bad.jpg", + + "$base/local-public/5/5f/LoremIpsum.djvu", + "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg", + "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg", + "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg", + + "$base/local-public/f/ff/Foobar.svg", + "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png", + "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png", + + "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", + ] + ); + } + + /** + * Delete the specified files, if they exist. + * @param array $files Full paths to files to delete. + */ + private static function deleteFiles( $files ) { + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + foreach ( $files as $file ) { + $backend->delete( [ 'src' => $file ], [ 'force' => 1 ] ); + } + foreach ( $files as $file ) { + $tmp = FileBackend::parentStoragePath( $file ); + while ( $tmp ) { + if ( !$backend->clean( [ 'dir' => $tmp ] )->isOK() ) { + break; + } + $tmp = FileBackend::parentStoragePath( $tmp ); + } + } + } + + protected function getBaseDir() { + return 'mwstore://local-backend'; + } + + public function parserTestProvider() { + if ( $this->file === false ) { + global $wgParserTestFiles; + $this->file = $wgParserTestFiles[0]; + } + + return new TestFileDataProvider( $this->file, $this ); + } + + /** + * Set the file from whose tests will be run by this instance + * @param string $filename + */ + public function setParserTestFile( $filename ) { + $this->file = $filename; + } + + /** + * @group medium + * @group ParserTests + * @dataProvider parserTestProvider + * @param string $desc + * @param string $input + * @param string $result + * @param array $opts + * @param array $config + */ + public function testParserTest( $desc, $input, $result, $opts, $config ) { + if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) { + $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions" + // $this->markTestSkipped( 'Filtered out by the user' ); + $this->teardownGlobals(); + return; + } + + if ( !$this->isWikitextNS( NS_MAIN ) ) { + // parser tests frequently assume that the main namespace contains wikitext. + // @todo When setting up pages, force the content model. Only skip if + // $wgtContentModelUseDB is false. + $this->teardownGlobals(); + $this->markTestSkipped( "Main namespace does not support wikitext," + . "skipping parser test: $desc" ); + } + + wfDebug( "Running parser test: $desc\n" ); + + $opts = $this->parseOptions( $opts ); + $context = $this->setupGlobals( $opts, $config ); + + $user = $context->getUser(); + $options = ParserOptions::newFromContext( $context ); + + if ( isset( $opts['title'] ) ) { + $titleText = $opts['title']; + } else { + $titleText = 'Parser test'; + } + + $local = isset( $opts['local'] ); + $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; + $parser = $this->getParser( $preprocessor ); + + $title = Title::newFromText( $titleText ); + + # Parser test requiring math. Make sure texvc is executable + # or just skip such tests. + if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) { + global $wgTexvc; + + if ( !isset( $wgTexvc ) ) { + $this->teardownGlobals(); + $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" ); + } elseif ( !is_executable( $wgTexvc ) ) { + $this->teardownGlobals(); + $this->markTestSkipped( "SKIPPED: texvc binary does not exist" + . " or is not executable.\n" + . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" ); + } + } + + if ( isset( $opts['djvu'] ) ) { + if ( !$this->djVuSupport->isEnabled() ) { + $this->teardownGlobals(); + $this->markTestSkipped( "SKIPPED: djvu binaries do not exist or are not executable.\n" ); + } + } + + if ( isset( $opts['tidy'] ) ) { + if ( !$this->tidySupport->isEnabled() ) { + $this->teardownGlobals(); + $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" ); + } else { + $options->setTidy( true ); + } + } + + if ( isset( $opts['pst'] ) ) { + $out = $parser->preSaveTransform( $input, $title, $user, $options ); + } elseif ( isset( $opts['msg'] ) ) { + $out = $parser->transformMsg( $input, $options, $title ); + } elseif ( isset( $opts['section'] ) ) { + $section = $opts['section']; + $out = $parser->getSection( $input, $section ); + } elseif ( isset( $opts['replace'] ) ) { + $section = $opts['replace'][0]; + $replace = $opts['replace'][1]; + $out = $parser->replaceSection( $input, $section, $replace ); + } elseif ( isset( $opts['comment'] ) ) { + $out = Linker::formatComment( $input, $title, $local ); + } elseif ( isset( $opts['preload'] ) ) { + $out = $parser->getPreloadText( $input, $title, $options ); + } else { + $output = $parser->parse( $input, $title, $options, true, true, 1337 ); + $output->setTOCEnabled( !isset( $opts['notoc'] ) ); + $out = $output->getText(); + if ( isset( $opts['tidy'] ) ) { + $out = preg_replace( '/\s+$/', '', $out ); + } + + if ( isset( $opts['showtitle'] ) ) { + if ( $output->getTitleText() ) { + $title = $output->getTitleText(); + } + + $out = "$title\n$out"; + } + + if ( isset( $opts['showindicators'] ) ) { + $indicators = ''; + foreach ( $output->getIndicators() as $id => $content ) { + $indicators .= "$id=$content\n"; + } + $out = $indicators . $out; + } + + if ( isset( $opts['ill'] ) ) { + $out = implode( ' ', $output->getLanguageLinks() ); + } elseif ( isset( $opts['cat'] ) ) { + $outputPage = $context->getOutput(); + $outputPage->addCategoryLinks( $output->getCategories() ); + $cats = $outputPage->getCategoryLinks(); + + if ( isset( $cats['normal'] ) ) { + $out = implode( ' ', $cats['normal'] ); + } else { + $out = ''; + } + } + $parser->mPreprocessor = null; + } + + $this->teardownGlobals(); + + $this->assertEquals( $result, $out, $desc ); + } + + /** + * Get a Parser object + * @param Preprocessor $preprocessor + * @return Parser + */ + function getParser( $preprocessor = null ) { + global $wgParserConf; + + $class = $wgParserConf['class']; + $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); + + Hooks::run( 'ParserTestParser', [ &$parser ] ); + + return $parser; + } + + // Various action functions + + public function addArticle( $name, $text, $line ) { + self::$articles[$name] = [ $text, $line ]; + } + + public function publishTestArticles() { + if ( empty( self::$articles ) ) { + return; + } + + foreach ( self::$articles as $name => $info ) { + list( $text, $line ) = $info; + ParserTestRunner::addArticle( $name, $text, $line, 'ignoreduplicate' ); + } + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param string $name + * @return bool True if tag hook is present + */ + public function requireHook( $name ) { + global $wgParser; + $wgParser->firstCallInit(); // make sure hooks are loaded. + return isset( $wgParser->mTagHooks[$name] ); + } + + public function requireFunctionHook( $name ) { + global $wgParser; + $wgParser->firstCallInit(); // make sure hooks are loaded. + return isset( $wgParser->mFunctionHooks[$name] ); + } + + public function requireTransparentHook( $name ) { + global $wgParser; + $wgParser->firstCallInit(); // make sure hooks are loaded. + return isset( $wgParser->mTransparentTagHooks[$name] ); + } + + // Various "cleanup" functions + + /** + * Remove last character if it is a newline + * @param string $s + * @return string + */ + public function removeEndingNewline( $s ) { + if ( substr( $s, -1 ) === "\n" ) { + return substr( $s, 0, -1 ); + } else { + return $s; + } + } + + // Test options parser functions + + protected function parseOptions( $instring ) { + $opts = []; + // foo + // foo=bar + // foo="bar baz" + // foo=[[bar baz]] + // foo=bar,"baz quux" + $regex = '/\b + ([\w-]+) # Key + \b + (?:\s* + = # First sub-value + \s* + ( + " + [^"]* # Quoted val + " + | + \[\[ + [^]]* # Link target + \]\] + | + [\w-]+ # Plain word + ) + (?:\s* + , # Sub-vals 1..N + \s* + ( + "[^"]*" # Quoted val + | + \[\[[^]]*\]\] # Link target + | + [\w-]+ # Plain word + ) + )* + )? + /x'; + + if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $bits ) { + array_shift( $bits ); + $key = strtolower( array_shift( $bits ) ); + if ( count( $bits ) == 0 ) { + $opts[$key] = true; + } elseif ( count( $bits ) == 1 ) { + $opts[$key] = $this->cleanupOption( array_shift( $bits ) ); + } else { + // Array! + $opts[$key] = array_map( [ $this, 'cleanupOption' ], $bits ); + } + } + } + + return $opts; + } + + protected function cleanupOption( $opt ) { + if ( substr( $opt, 0, 1 ) == '"' ) { + return substr( $opt, 1, -1 ); + } + + if ( substr( $opt, 0, 2 ) == '[[' ) { + return substr( $opt, 2, -2 ); + } + + return $opt; + } + + /** + * Use a regex to find out the value of an option + * @param string $key Name of option val to retrieve + * @param array $opts Options array to look in + * @param mixed $default Default value returned if not found + * @return mixed + */ + protected static function getOptionValue( $key, $opts, $default ) { + $key = strtolower( $key ); + + if ( isset( $opts[$key] ) ) { + return $opts[$key]; + } else { + return $default; + } + } +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php index 4158863505..acd8575150 100755 --- a/tests/phpunit/phpunit.php +++ b/tests/phpunit/phpunit.php @@ -79,7 +79,7 @@ class PHPUnitMaintClass extends Maintenance { global $wgAuthManagerConfig, $wgAuth; // Inject test autoloader - require_once __DIR__ . '/../TestsAutoLoader.php'; + require_once __DIR__ . '/../common/TestsAutoLoader.php'; // wfWarn should cause tests to fail $wgDevelopmentWarnings = true; diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index ed18205753..6443ec40d6 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -25,7 +25,7 @@ languages - includes/parser/MediaWikiParserTest.php + suites/ParserTestTopLevelSuite.php suites/ExtensionsParserTestSuite.php diff --git a/tests/phpunit/suites/ExtensionsParserTestSuite.php b/tests/phpunit/suites/ExtensionsParserTestSuite.php index 3d68b24198..8d6ee07956 100644 --- a/tests/phpunit/suites/ExtensionsParserTestSuite.php +++ b/tests/phpunit/suites/ExtensionsParserTestSuite.php @@ -2,7 +2,7 @@ class ExtensionsParserTestSuite extends PHPUnit_Framework_TestSuite { public static function suite() { - return MediaWikiParserTest::suite( MediaWikiParserTest::NO_CORE ); + return ParserTestTopLevelSuite::suite( ParserTestTopLevelSuite::NO_CORE ); } } diff --git a/tests/phpunit/suites/ParserTestTopLevelSuite.php b/tests/phpunit/suites/ParserTestTopLevelSuite.php new file mode 100644 index 0000000000..36ecf73a71 --- /dev/null +++ b/tests/phpunit/suites/ParserTestTopLevelSuite.php @@ -0,0 +1,138 @@ + "\\'", '\\' => '\\\\' ] ); + $parserTestClassName = ucfirst( $testsName ); + + // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php + // Prepend 'ParserTest_' to be paranoid about it not starting with a number + $parserTestClassName = 'ParserTest_' . + preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName ); + + if ( isset( $testList[$parserTestClassName] ) ) { + // If a conflict happens, gives a very unclear fatal. + // So as a last ditch effort to prevent that eventuality, if there + // is a conflict, append a number. + $counter++; + $parserTestClassName .= $counter; + } + $testList[$parserTestClassName] = true; + $parserTestClassDefinition = <<addTestSuite( $parserTestClassName ); + } + return $suite; + } + + /** + * Write $msg under log group 'tests-parser' + * @param string $msg Message to log + */ + protected static function debug( $msg ) { + return wfDebugLog( 'tests-parser', wfGetCaller() . ' ' . $msg ); + } +}