Merge "Make $wgRevisionCacheExpiry default to one week"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 9 Sep 2016 10:37:49 +0000 (10:37 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 9 Sep 2016 10:37:49 +0000 (10:37 +0000)
87 files changed:
includes/DefaultSettings.php
includes/EditPage.php
includes/FauxRequest.php
includes/LinkFilter.php
includes/OutputPage.php
includes/Revision.php
includes/Title.php
includes/WebRequest.php
includes/actions/InfoAction.php
includes/api/ApiEditPage.php
includes/api/i18n/de.json
includes/api/i18n/lij.json [new file with mode: 0644]
includes/auth/AuthManager.php
includes/cache/LinkBatch.php
includes/content/ContentHandler.php
includes/content/JsonContentHandler.php
includes/dao/DBAccessObjectUtils.php
includes/db/DBConnRef.php
includes/db/Database.php
includes/db/DatabasePostgres.php
includes/db/IDatabase.php
includes/db/loadbalancer/LoadBalancer.php
includes/filebackend/lockmanager/LockManager.php
includes/filebackend/lockmanager/MemcLockManager.php
includes/installer/i18n/lij.json [new file with mode: 0644]
includes/interwiki/InterwikiLookup.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/libs/WaitConditionLoop.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/page/WikiPage.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/specials/SpecialChangeContentModel.php
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bn.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/egl.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/hy.json
languages/i18n/ja.json
languages/i18n/lij.json
languages/i18n/lv.json
languages/i18n/qqq.json
languages/i18n/tcy.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/zh-hant.json
resources/src/mediawiki/api/rollback.js
resources/src/mediawiki/page/rollback.js
tests/TestsAutoLoader.php
tests/browser/environments.yml
tests/parser/DbTestPreviewer.php [new file with mode: 0644]
tests/parser/DbTestRecorder.php [new file with mode: 0644]
tests/parser/DelayedParserTest.php [new file with mode: 0644]
tests/parser/DjVuSupport.php [new file with mode: 0644]
tests/parser/ITestRecorder.php [new file with mode: 0644]
tests/parser/ParserTest.php [new file with mode: 0644]
tests/parser/ParserTestParserHook.php [new file with mode: 0644]
tests/parser/ParserTestResultNormalizer.php [new file with mode: 0644]
tests/parser/TestFileDataProvider.php [new file with mode: 0644]
tests/parser/TestFileIterator.php [new file with mode: 0644]
tests/parser/TestRecorder.php [new file with mode: 0644]
tests/parser/TidySupport.php [new file with mode: 0644]
tests/parser/fuzzTest.php [new file with mode: 0644]
tests/parser/parserTest.inc [deleted file]
tests/parser/parserTestsParserHook.php [deleted file]
tests/parserTests.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/FauxRequestTest.php
tests/phpunit/includes/WebRequestTest.php
tests/phpunit/includes/api/ApiEditPageTest.php
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/includes/content/JsonContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/db/DatabaseTest.php
tests/phpunit/includes/libs/WaitConditionLoopTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/parser/NewParserTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
tests/phpunit/phpunit.php
tests/testHelpers.inc [deleted file]

index 03947f6..c7fda14 100644 (file)
@@ -5581,6 +5581,11 @@ $wgRateLimits = [
                'ip' => [ 8, 60 ],
                'newbie' => [ 8, 60 ],
        ],
+       // Changing the content model of a page
+       'editcontentmodel' => [
+               'newbie' => [ 2, 120 ],
+               'user' => [ 8, 60 ],
+       ],
 ];
 
 /**
index b98c908..7e4e411 100644 (file)
@@ -562,16 +562,29 @@ class EditPage {
 
                $revision = $this->mArticle->getRevisionFetched();
                // Disallow editing revisions with content models different from the current one
-               if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
-                       $this->displayViewSourcePage(
-                               $this->getContentObject(),
-                               wfMessage(
-                                       'contentmodelediterror',
-                                       $revision->getContentModel(),
-                                       $this->contentModel
-                               )->plain()
-                       );
-                       return;
+               // Undo edits being an exception in order to allow reverting content model changes.
+               if ( $revision
+                       && $revision->getContentModel() !== $this->contentModel
+               ) {
+                       $prevRev = null;
+                       if ( $this->undidRev ) {
+                               $undidRevObj = Revision::newFromId( $this->undidRev );
+                               $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
+                       }
+                       if ( !$this->undidRev
+                               || !$prevRev
+                               || $prevRev->getContentModel() !== $this->contentModel
+                       ) {
+                               $this->displayViewSourcePage(
+                                       $this->getContentObject(),
+                                       wfMessage(
+                                               'contentmodelediterror',
+                                               $revision->getContentModel(),
+                                               $this->contentModel
+                                       )->plain()
+                               );
+                               return;
+                       }
                }
 
                $this->isConflict = false;
@@ -1134,6 +1147,14 @@ class EditPage {
                                                        $oldContent = $this->page->getContent( Revision::RAW );
                                                        $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
                                                        $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
+                                                       if ( $newContent->getModel() !== $oldContent->getModel() ) {
+                                                               // The undo may change content
+                                                               // model if its reverting the top
+                                                               // edit. This can result in
+                                                               // mismatched content model/format.
+                                                               $this->contentModel = $newContent->getModel();
+                                                               $this->contentFormat = $oldrev->getContentFormat();
+                                                       }
 
                                                        if ( $newContent->equals( $oldContent ) ) {
                                                                # Tell the user that the undo results in no change,
@@ -1264,9 +1285,11 @@ class EditPage {
                        $handler = ContentHandler::getForModelID( $this->contentModel );
 
                        return $handler->makeEmptyContent();
-               } else {
+               } elseif ( !$this->undidRev ) {
                        // Content models should always be the same since we error
-                       // out if they are different before this point.
+                       // out if they are different before this point (in ->edit()).
+                       // The exception being, during an undo, the current revision might
+                       // differ from the prior revision.
                        $logger = LoggerFactory::getInstance( 'editpage' );
                        if ( $this->contentModel !== $rev->getContentModel() ) {
                                $logger->warning( "Overriding content model from current edit {prev} to {new}", [
@@ -1290,9 +1313,8 @@ class EditPage {
                                ] );
                                $this->contentFormat = $rev->getContentFormat();
                        }
-
-                       return $content;
                }
+               return $content;
        }
 
        /**
@@ -1836,7 +1858,9 @@ class EditPage {
                        $status->value = self::AS_READ_ONLY_PAGE;
                        return $status;
                }
-               if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
+               if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 )
+                       || ( $changingContentModel && $wgUser->pingLimiter( 'editcontentmodel' ) )
+               ) {
                        $status->fatal( 'actionthrottledtext' );
                        $status->value = self::AS_RATE_LIMITED;
                        return $status;
index 158c852..3b2283b 100644 (file)
@@ -226,6 +226,7 @@ class FauxRequest extends WebRequest {
        }
 
        /**
+        * @codeCoverageIgnore
         * @param array $extWhitelist
         * @return bool
         */
@@ -234,6 +235,7 @@ class FauxRequest extends WebRequest {
        }
 
        /**
+        * @codeCoverageIgnore
         * @return string
         */
        protected function getRawIP() {
index d86291a..7b3d72b 100644 (file)
@@ -89,7 +89,7 @@ class LinkFilter {
         *
         * @param string $filterEntry Domainparts
         * @param string $protocol Protocol (default http://)
-        * @return array Array to be passed to Database::buildLike() or false on error
+        * @return array|bool Array to be passed to Database::buildLike() or false on error
         */
        public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
                $db = wfGetDB( DB_REPLICA );
index 6ae2a92..9b2d8da 100644 (file)
@@ -2679,16 +2679,29 @@ class OutputPage extends ContextSource {
                        // Prepare exempt modules for buildExemptModules()
                        $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
                        $exemptStates = [];
-                       $moduleStyles = array_filter( $this->getModuleStyles( /*filter*/ true ),
+                       $moduleStyles = $this->getModuleStyles( /*filter*/ true );
+
+                       // Batch preload getTitleInfo for isKnownEmpty() calls below
+                       $exemptModules = array_filter( $moduleStyles,
+                               function ( $name ) use ( $rl, &$exemptGroups ) {
+                                       $module = $rl->getModule( $name );
+                                       return $module && isset( $exemptGroups[ $module->getGroup() ] );
+                               }
+                       );
+                       ResourceLoaderWikiModule::preloadTitleInfo(
+                               $context, wfGetDB( DB_REPLICA ), $exemptModules );
+
+                       // Filter out modules handled by buildExemptModules()
+                       $moduleStyles = array_filter( $moduleStyles,
                                function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
                                        $module = $rl->getModule( $name );
                                        if ( $module ) {
-                                               $group = $module->getGroup();
                                                if ( $name === 'user.styles' && $this->isUserCssPreview() ) {
                                                        $exemptStates[$name] = 'ready';
                                                        // Special case in buildExemptModules()
                                                        return false;
                                                }
+                                               $group = $module->getGroup();
                                                if ( isset( $exemptGroups[$group] ) ) {
                                                        $exemptStates[$name] = 'ready';
                                                        if ( !$module->isKnownEmpty( $context ) ) {
index af45acc..6acc528 100644 (file)
@@ -1609,25 +1609,38 @@ class Revision implements IDBAccessObject {
                        $row = null;
                }
 
+               // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
+               // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
+               $flags = $this->mQueryFlags;
+               $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
+                       ? self::READ_LATEST_IMMUTABLE
+                       : 0;
+
+               list( $index, $options, $fallbackIndex, $fallbackOptions ) =
+                       DBAccessObjectUtils::getDBOptions( $flags );
+
                if ( !$row ) {
                        // Text data is immutable; check replica DBs first.
-                       $dbr = wfGetDB( DB_REPLICA );
-                       $row = $dbr->selectRow( 'text',
+                       $row = wfGetDB( $index )->selectRow(
+                               'text',
                                [ 'old_text', 'old_flags' ],
                                [ 'old_id' => $textId ],
-                               __METHOD__ );
+                               __METHOD__,
+                               $options
+                       );
                }
 
-               // Fallback to the master in case of replica DB lag. Also use FOR UPDATE if it was
-               // used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
-               $forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
-               if ( !$row && ( $forUpdate || wfGetLB()->getServerCount() > 1 ) ) {
-                       $dbw = wfGetDB( DB_MASTER );
-                       $row = $dbw->selectRow( 'text',
+               // Fallback to DB_MASTER in some cases if the row was not found
+               if ( !$row && $fallbackIndex !== null ) {
+                       // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
+                       // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
+                       $row = wfGetDB( $fallbackIndex )->selectRow(
+                               'text',
                                [ 'old_text', 'old_flags' ],
                                [ 'old_id' => $textId ],
                                __METHOD__,
-                               $forUpdate ? [ 'FOR UPDATE' ] : [] );
+                               $fallbackOptions
+                       );
                }
 
                if ( !$row ) {
index 5e5a1b7..3475b26 100644 (file)
@@ -751,12 +751,12 @@ class Title implements LinkTarget {
        /**
         * Callback for usort() to do title sorts by (namespace, title)
         *
-        * @param Title $a
-        * @param Title $b
+        * @param LinkTarget $a
+        * @param LinkTarget $b
         *
         * @return int Result of string comparison, or namespace comparison
         */
-       public static function compare( $a, $b ) {
+       public static function compare( LinkTarget $a, LinkTarget $b ) {
                if ( $a->getNamespace() == $b->getNamespace() ) {
                        return strcmp( $a->getText(), $b->getText() );
                } else {
index 5492737..8f78164 100644 (file)
@@ -83,6 +83,9 @@ class WebRequest {
        /** @var bool Whether this HTTP request is "safe" (even if it is an HTTP post) */
        protected $markedAsSafe = false;
 
+       /**
+        * @codeCoverageIgnore
+        */
        public function __construct() {
                $this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
                        ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
@@ -351,7 +354,7 @@ class WebRequest {
         * @return array|string Cleaned-up version of the given
         * @private
         */
-       function normalizeUnicode( $data ) {
+       public function normalizeUnicode( $data ) {
                if ( is_array( $data ) ) {
                        foreach ( $data as $key => $val ) {
                                $data[$key] = $this->normalizeUnicode( $val );
@@ -641,6 +644,7 @@ class WebRequest {
         * Get the values passed in the query string.
         * No transformation is performed on the values.
         *
+        * @codeCoverageIgnore
         * @return array
         */
        public function getQueryValues() {
@@ -651,6 +655,7 @@ class WebRequest {
         * Return the contents of the Query with no decoding. Use when you need to
         * know exactly what was sent, e.g. for an OAuth signature over the elements.
         *
+        * @codeCoverageIgnore
         * @return string
         */
        public function getRawQueryString() {
index 43bff87..abc7cb2 100644 (file)
@@ -202,6 +202,7 @@ class InfoAction extends FormlessAction {
                $title = $this->getTitle();
                $id = $title->getArticleID();
                $config = $this->context->getConfig();
+               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
 
                $pageCounts = $this->pageCounts( $this->page );
 
@@ -279,9 +280,18 @@ class InfoAction extends FormlessAction {
                        . ' ' . $this->msg( 'parentheses', $pageLang )->escaped() ];
 
                // Content model of the page
+               $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
+               // If the user can change it, add a link to Special:ChangeContentModel
+               if ( $title->quickUserCan( 'editcontentmodel' ) ) {
+                       $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
+                               SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
+                               $this->msg( 'pageinfo-content-model-change' )->text()
+                       ) )->escaped();
+               }
+
                $pageInfo['header-basic'][] = [
                        $this->msg( 'pageinfo-content-model' ),
-                       htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) )
+                       $modelHtml
                ];
 
                // Search engine status
index 00daba9..ee9150c 100644 (file)
@@ -97,6 +97,7 @@ class ApiEditPage extends ApiBase {
                } else {
                        $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
                }
+               $contentModel = $contentHandler->getModelID();
 
                $name = $titleObj->getPrefixedDBkey();
                $model = $contentHandler->getModelID();
@@ -111,11 +112,11 @@ class ApiEditPage extends ApiBase {
                }
 
                if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
-                       $params['contentformat'] = $contentHandler->getDefaultFormat();
+                       $contentFormat = $contentHandler->getDefaultFormat();
+               } else {
+                       $contentFormat = $params['contentformat'];
                }
 
-               $contentFormat = $params['contentformat'];
-
                if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
 
                        $this->dieUsage( "The requested format $contentFormat is not supported for content model " .
@@ -265,9 +266,21 @@ class ApiEditPage extends ApiBase {
                        if ( !$newContent ) {
                                $this->dieUsageMsg( 'undo-failure' );
                        }
-
-                       $params['text'] = $newContent->serialize( $params['contentformat'] );
-
+                       if ( empty( $params['contentmodel'] )
+                               && empty( $params['contentformat'] )
+                       ) {
+                               // If we are reverting content model, the new content model
+                               // might not support the current serialization format, in
+                               // which case go back to the old serialization format,
+                               // but only if the user hasn't specified a format/model
+                               // parameter.
+                               if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
+                                       $contentFormat = $undoafterRev->getContentFormat();
+                               }
+                               // Override content model with model of undid revision.
+                               $contentModel = $newContent->getModel();
+                       }
+                       $params['text'] = $newContent->serialize( $contentFormat );
                        // If no summary was given and we only undid one rev,
                        // use an autosummary
                        if ( is_null( $params['summary'] ) &&
@@ -288,7 +301,7 @@ class ApiEditPage extends ApiBase {
                $requestArray = [
                        'wpTextbox1' => $params['text'],
                        'format' => $contentFormat,
-                       'model' => $contentHandler->getModelID(),
+                       'model' => $contentModel,
                        'wpEditToken' => $params['token'],
                        'wpIgnoreBlankSummary' => true,
                        'wpIgnoreBlankArticle' => true,
index 06311f9..41f1ff9 100644 (file)
        "apihelp-query+allusers-param-witheditsonly": "Listet nur Benutzer auf, die Bearbeitungen vorgenommen haben.",
        "apihelp-query+allusers-param-activeusers": "Listet nur Benutzer auf, die in den letzten $1 {{PLURAL:$1|Tag|Tagen}} aktiv waren.",
        "apihelp-query+allusers-example-Y": "Benutzer ab <kbd>Y</kbd> auflisten.",
+       "apihelp-query+authmanagerinfo-example-login": "Ruft die Anfragen ab, die beim Beginnen einer Anmeldung verwendet werden können.",
+       "apihelp-query+authmanagerinfo-example-login-merged": "Ruft die Anfragen ab, die beim Beginnen einer Anmeldung verwendet werden können, mit zusammengeführten Formularfeldern.",
+       "apihelp-query+authmanagerinfo-example-securitysensitiveoperation": "Testet, ob die Authentifizierung für die Aktion <kbd>foo</kbd> ausreichend ist.",
        "apihelp-query+backlinks-description": "Alle Seiten finden, die auf die angegebene Seite verlinken.",
        "apihelp-query+backlinks-param-title": "Zu suchender Titel. Darf nicht zusammen mit <var>$1pageid</var> benutzt werden.",
        "apihelp-query+backlinks-param-pageid": "Zu suchende Seiten-ID. Darf nicht zusammen mit <var>$1title</var> benutzt werden.",
        "apihelp-query+watchlist-paramvalue-prop-sizes": "Ergänzt die alten und neuen Längen der Seite.",
        "apihelp-query+watchlist-paramvalue-type-new": "Seitenerstellungen.",
        "apihelp-query+watchlist-paramvalue-type-log": "Logbucheinträge.",
+       "apihelp-query+watchlistraw-description": "Ruft alle Seiten der Beobachtungsliste des aktuellen Benutzers ab.",
        "apihelp-query+watchlistraw-param-prop": "Zusätzlich zurückzugebende Eigenschaften:",
+       "apihelp-query+watchlistraw-param-fromtitle": "Titel (mit Namensraum-Präfix), bei dem die Aufzählung beginnen soll.",
+       "apihelp-query+watchlistraw-param-totitle": "Titel (mit Namensraum-Präfix), bei dem die Aufzählung enden soll.",
        "apihelp-resetpassword-param-user": "Benutzer, der zurückgesetzt werden soll.",
+       "apihelp-revisiondelete-description": "Löscht und stellt Versionen wieder her.",
+       "apihelp-revisiondelete-param-hide": "Was für jede Version versteckt werden soll.",
+       "apihelp-revisiondelete-param-show": "Was für jede Version wieder eingeblendet werden soll.",
        "apihelp-rsd-description": "Ein RSD-Schema (Really Simple Discovery) exportieren.",
        "apihelp-rsd-example-simple": "Das RSD-Schema exportieren",
        "apihelp-setnotificationtimestamp-param-entirewatchlist": "An allen beobachteten Seiten arbeiten.",
        "apihelp-stashedit-param-sectiontitle": "Der Titel für einen neuen Abschnitt.",
        "apihelp-stashedit-param-text": "Seiteninhalt.",
        "apihelp-stashedit-param-stashedtexthash": "Stattdessen zu verwendende Prüfsumme des Seiteninhalts von einem vorherigen Speicher.",
+       "apihelp-stashedit-param-contentmodel": "Inhaltsmodell des neuen Inhalts.",
        "apihelp-stashedit-param-summary": "Änderungszusammenfassung.",
        "apihelp-tag-param-reason": "Grund für die Änderung.",
        "apihelp-tokens-param-type": "Abzufragende Tokentypen.",
diff --git a/includes/api/i18n/lij.json b/includes/api/i18n/lij.json
new file mode 100644 (file)
index 0000000..5acd38a
--- /dev/null
@@ -0,0 +1,103 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Giromin Cangiaxo"
+               ]
+       },
+       "apihelp-main-param-action": "Açion da compî.",
+       "apihelp-main-param-format": "Formato de l'output.",
+       "apihelp-main-param-assert": "Veifica che l'utente o l'agge effetoòu l'accesso se s'è impostou <kbd>user</kbd>, ò ch'o l'agge i permissi di bot se s'è impostou <kbd>bot</kbd>.",
+       "apihelp-main-param-requestid": "Tutti i valoî fornii saian incruxi inta risposta. Porieivan ese doeuviæ pe distingue e receste.",
+       "apihelp-main-param-servedby": "Inciodi into risultou o nomme de l'host ch'o l'ha servio a recesta.",
+       "apihelp-main-param-curtimestamp": "Inciodi into risultou o timestamp attoâ.",
+       "apihelp-block-description": "Blocca un utente.",
+       "apihelp-block-param-user": "Nomme utente, adresso IP o range di IP da bloccâ.",
+       "apihelp-block-param-expiry": "Tempo de scadença. O poeu ese relativo (presempio, <kbd>5 months</kbd> o <kbd>2 weeks</kbd>) ò assoluo (presempio <kbd>2014-09-18T12:34:56Z</kbd>). Se impostou a <kbd>infinite</kbd>, <kbd>indefinite</kbd> ò <kbd>never</kbd>, o blòcco o no descaziâ mai.",
+       "apihelp-block-param-reason": "Raxon do blòcco.",
+       "apihelp-block-param-anononly": "Blocca solo che i utenti non registræ (saiv'a dî disattiva i contributi anonnimi da questo adresso IP).",
+       "apihelp-block-param-nocreate": "Impedisci a creaçion de utençe.",
+       "apihelp-block-param-autoblock": "Blocca aotomaticamente l'urtimo adreçço IP doeuviou da l'utente e i succescivi co-i quæ tentan l'accesso",
+       "apihelp-block-param-hidename": "Ascondi o nomme utente da-o registro di blocchi (Ghe voeu i permissi de <code>hideuser</code>).",
+       "apihelp-block-param-reblock": "Se l'utente o l'è za bloccou, sorvescrive o blocco existente.",
+       "apihelp-block-param-watchuser": "Oserva a paggina utente e e paggine de discuscion utente de l'utente ò de l'adresso IP.",
+       "apihelp-block-example-ip-simple": "Blocca l'adresso IP <kbd>192.0.2.5</kbd> pe trei giorni con motivaçion <kbd>First strike</kbd>.",
+       "apihelp-block-example-user-complex": "Blocca l'utente <kbd>Vandal</kbd> a tempo indeterminou con motivaçion <kbd>Vandalism</kbd>, e impediscighe a creaçion de noeuve utençe e l'invio de e-mail.",
+       "apihelp-changeauthenticationdata-description": "Modificâ i dæti d'aotenticaçion pe l'utente corente.",
+       "apihelp-changeauthenticationdata-example-password": "Tentativo de modificâ a password de l'utente corente a <kbd>ExamplePassword</kbd>.",
+       "apihelp-checktoken-description": "Veifica a validitæ de 'n token da <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
+       "apihelp-checktoken-param-type": "Tipo de token in corso de test.",
+       "apihelp-checktoken-param-token": "Token da testâ.",
+       "apihelp-checktoken-param-maxtokenage": "Mascima etæ consentia pe-o token, in segondi.",
+       "apihelp-checktoken-example-simple": "Veifica a validitæ de 'n token <kbd>csrf</kbd>.",
+       "apihelp-clearhasmsg-description": "Scassa o flag <code>hasmsg</code> pe l'utente corente.",
+       "apihelp-clearhasmsg-example-1": "Scassa o flag <code>hasmsg</code> pe l'utente corente.",
+       "apihelp-clientlogin-description": "Accedi a-o wiki doeuviando o flusso interattivo.",
+       "apihelp-clientlogin-example-login": "Avvia o processo d'accesso a-a wiki comme utente <kbd>Example</kbd> con password <kbd>ExamplePassword</kbd>.",
+       "apihelp-clientlogin-example-login2": "Continnoa l'accesso doppo una risposta de l'<samp>UI</samp> pe l'aotenticaçion a doî fattoî, fornindo un <var>OATHToken</var> de <kbd>987654</kbd>.",
+       "apihelp-compare-description": "Otegni e differençe tra 2 paggine.\n\nUn nummero de revixon, o tittolo de 'na paggina, ò un ID de paggina o dev'ese indicou segge pe-o \"da\" che pe-o \"a\".",
+       "apihelp-compare-param-fromtitle": "Primmo tittolo da confrontâ.",
+       "apihelp-compare-param-fromid": "Primo ID de paggina da confrontâ.",
+       "apihelp-compare-param-fromrev": "Primma revixon da confrontâ.",
+       "apihelp-compare-param-totitle": "Segondo tittolo da confrontâ.",
+       "apihelp-compare-param-toid": "Segondo ID de paggina da confrontâ.",
+       "apihelp-compare-param-torev": "Segonda revixon da confrontâ.",
+       "apihelp-compare-example-1": "Crea un diff tra revixon 1 e revixon 2.",
+       "apihelp-createaccount-description": "Crea una noeuva utença.",
+       "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> o l'ha restituto true pe <samp>hasprimarypreservedstate</samp>, e receste contrssegnæ comme <samp>primary-required</samp> dovieivan ese omisse. Se invece o l'ha restituio un valô non voeuo pe <samp>preservedusername</samp>, quello nomme utente o dev'ese doeuviou pe-o parammetro <var>username</var>.",
+       "apihelp-createaccount-example-create": "Avvia o processo de creaçion d'utente <kbd>Example</kbd> con password <kbd>ExamplePassword</kbd>.",
+       "apihelp-createaccount-param-name": "Nomme utente",
+       "apihelp-createaccount-param-password": "Password (a saiâ ignorâ se l'è impostou <var>$1mailpassword</var>).",
+       "apihelp-createaccount-param-domain": "Dominnio pe l'aotenticaçion esterna (opçionâ).",
+       "apihelp-createaccount-param-email": "Adresso Email de l'utente (opçionâ).",
+       "apihelp-createaccount-param-realname": "Nomme veo de l'utente (opçionâ).",
+       "apihelp-createaccount-param-mailpassword": "Se impostou insce 'n qualonque valô, una password random (caxoâ) a saiâ inviâ a l'utente.",
+       "apihelp-createaccount-param-reason": "Raxon facortativa da creaçion de l'utença da insei inti registri.",
+       "apihelp-createaccount-param-language": "Codiçe de lengua da impostâ comme predefinia pe l'utente (opçionâ, pe difetto a l'è a lengua do contegnuo).",
+       "apihelp-createaccount-example-pass": "Crea l'utente <kbd>testuser</kbd> con password <kbd>test123</kbd>.",
+       "apihelp-createaccount-example-mail": "Crea l'utente <kbd>testmailuser</kbd> e mandighe via e-mail una password generâ abrettio.",
+       "apihelp-delete-description": "Scassa 'na paggina",
+       "apihelp-delete-param-title": "Tittolo da paggina che se dexîa eliminâ. O no poeu vese doeuviou insemme a <var>$1pageid</var>.",
+       "apihelp-delete-param-pageid": "ID de paggina da paggina da scassâ. O no poeu vese doeuviou insemme con <var>$1title</var>.",
+       "apihelp-delete-param-reason": "Raxon da scassatua. S'a no saiâ indicâ, saiâ doeuviou 'na raxon generâ aotomaticamente.",
+       "apihelp-delete-param-watch": "O l'azonze a paggina a-a lista di oservæ speciali de l'utente corente.",
+       "apihelp-delete-param-unwatch": "O rimoeuve a pagina da-a lista di oservæ speciali de l'utente corente.",
+       "apihelp-delete-param-oldimage": "O nomme da vegia inmaggine da scassâ, comme fornia da [[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]].",
+       "apihelp-delete-example-simple": "Scassa <kbd>Main Page</kbd>.",
+       "apihelp-delete-example-reason": "Scassa a <kbd>Main Page</kbd> con motivaçion <kbd>Preparing for move</kbd>.",
+       "apihelp-disabled-description": "Questo modulo o l'è stæto disabilitou.",
+       "apihelp-edit-description": "Crea e modifica paggine.",
+       "apihelp-edit-param-title": "Tittolo da paggina da modificâ. O no poeu vese doeuviou insemme a <var>$1pageid</var>.",
+       "apihelp-edit-param-pageid": "ID de paggina da paggina da modificâ. O no poeu ese doeuviou insemme a <var>$1title</var>.",
+       "apihelp-edit-param-section": "Nummero de seçion. <kbd>0</kbd> pe-a seçion de d'ato, <kbd>new</kbd> pe 'na noeuva seçion.",
+       "apihelp-edit-param-sectiontitle": "O tittolo pe 'na noeuva seçion.",
+       "apihelp-edit-param-text": "Contegnuo da paggina.",
+       "apihelp-edit-param-summary": "Ogetto da modiffica. E ascì tittolo da seçion se $1sezione=new e $1sectiontitle o no l'è impostou.",
+       "apihelp-edit-param-tags": "Cangia i tag da apricâ a-a revixon.",
+       "apihelp-edit-param-minor": "Cangiamento menô.",
+       "apihelp-edit-param-notminor": "Cangiamento non-menô.",
+       "apihelp-edit-param-bot": "Marca sta modiffica comme bot.",
+       "apihelp-edit-param-createonly": "No modificâ a paggina s'a l'existe za.",
+       "apihelp-edit-param-nocreate": "O genera un errô se a paggina a no l'existe.",
+       "apihelp-edit-param-watch": "O l'azonze a paggina a-a lista di oservæ speciali de l'utente corente.",
+       "apihelp-edit-param-unwatch": "O rimoeuve a pagina da-a lista di oservæ speciali de l'utente corente.",
+       "apihelp-edit-param-redirect": "Resciorvi aotomaticamente i rimandi.",
+       "apihelp-edit-param-contentmodel": "Modello de contegnuo di noeuvi contegnui.",
+       "apihelp-edit-param-token": "O token o dev'ese delongo inviou comme urtimo parammetro, ò aomeno doppo o parametro $1text.",
+       "apihelp-edit-example-edit": "Modiffica 'na paggina.",
+       "apihelp-edit-example-prepend": "Antepon-i <kbd>_&#95;NOTOC_&#95;</kbd> a 'na paggina.",
+       "apihelp-emailuser-description": "Manda 'n'e-mail a 'n utente.",
+       "apihelp-emailuser-param-target": "Utente a chi inviâ l'e-mail.",
+       "apihelp-emailuser-param-subject": "Ogetto de l'e-mail.",
+       "apihelp-emailuser-param-text": "Testo de l'e-mail.",
+       "apihelp-emailuser-param-ccme": "Mandime una copia de questa mail.",
+       "apihelp-emailuser-example-email": "Manda un'e-mail a l'utente <kbd>WikiSysop</kbd> co-o testo <kbd>Content</kbd>.",
+       "apihelp-expandtemplates-description": "Espandi tutti i template into wikitesto.",
+       "apihelp-expandtemplates-param-title": "Tittolo da paggina.",
+       "apihelp-expandtemplates-param-text": "Wikitesto da convertî.",
+       "apihelp-expandtemplates-param-prop": "Quæ informaçion otegnî.\n\nNotta che se no l'è seleçionou arcun valô, o risultou o contegniâ o codiçe wiki, ma l'output o saiâ inte 'n formato obsoleto.",
+       "apihelp-expandtemplates-paramvalue-prop-wikitext": "O wikitext espanso.",
+       "apihelp-expandtemplates-paramvalue-prop-properties": "Propietæ da paggina definie da-e paole magiche esteise into wikitesto.",
+       "apihelp-expandtemplates-paramvalue-prop-volatile": "Se l'output o segge volatile e o no 'agge da ese riadoeuviou atr'onde a l'interno da paggina.",
+       "apihelp-expandtemplates-paramvalue-prop-ttl": "O tempo mascimo doppo o quæ e memoizaçioin tempoannie (cache) do risultou dovieivan ese invalidæ.",
+       "apihelp-feedcontributions-param-feedformat": "O formato do feed."
+}
index 992e70f..89a22f8 100644 (file)
@@ -1679,14 +1679,12 @@ class AuthManager implements LoggerAwareInterface {
                try {
                        $status = $user->addToDatabase();
                        if ( !$status->isOk() ) {
-                               // double-check for a race condition (T70012)
-                               $localId = User::idFromName( $username, User::READ_LATEST );
-                               if ( $localId ) {
+                               // Double-check for a race condition (T70012). We make use of the fact that when
+                               // addToDatabase fails due to the user already existing, the user object gets loaded.
+                               if ( $user->getId() ) {
                                        $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
                                                'username' => $username,
                                        ] );
-                                       $user->setId( $localId );
-                                       $user->loadFromId( User::READ_LATEST );
                                        if ( $login ) {
                                                $this->setSessionDataForUser( $user );
                                        }
index e6d8630..8a4d061 100644 (file)
@@ -40,7 +40,11 @@ class LinkBatch {
         */
        protected $caller;
 
-       function __construct( $arr = [] ) {
+       /**
+        * LinkBatch constructor.
+        * @param LinkTarget[] $arr Initial items to be added to the batch
+        */
+       public function __construct( $arr = [] ) {
                foreach ( $arr as $item ) {
                        $this->addObj( $item );
                }
index 22db08a..4e50c8e 100644 (file)
@@ -453,10 +453,6 @@ abstract class ContentHandler {
        public function __construct( $modelId, $formats ) {
                $this->mModelID = $modelId;
                $this->mSupportedFormats = $formats;
-
-               $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
-               $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
-               $this->mModelName = strtolower( $this->mModelName );
        }
 
        /**
@@ -1018,9 +1014,21 @@ abstract class ContentHandler {
                        return false; // no content to undo
                }
 
-               $this->checkModelID( $cur_content->getModel() );
-               $this->checkModelID( $undo_content->getModel() );
-               $this->checkModelID( $undoafter_content->getModel() );
+               try {
+                       $this->checkModelID( $cur_content->getModel() );
+                       $this->checkModelID( $undo_content->getModel() );
+                       if ( $current->getId() !== $undo->getId() ) {
+                               // If we are undoing the most recent revision,
+                               // its ok to revert content model changes. However
+                               // if we are undoing a revision in the middle, then
+                               // doing that will be confusing.
+                               $this->checkModelID( $undoafter_content->getModel() );
+                       }
+               } catch ( MWException $e ) {
+                       // If the revisions have different content models
+                       // just return false
+                       return false;
+               }
 
                if ( $cur_content->equals( $undo_content ) ) {
                        // No use doing a merge if it's just a straight revert.
index eb1c67d..edb21f6 100644 (file)
@@ -39,4 +39,9 @@ class JsonContentHandler extends CodeContentHandler {
        protected function getContentClass() {
                return JsonContent::class;
        }
+
+       public function makeEmptyContent() {
+               $class = $this->getContentClass();
+               return new $class( '{}' );
+       }
 }
index 1fc9ae7..cc63446 100644 (file)
@@ -26,7 +26,7 @@
  *
  * @since 1.26
  */
-class DBAccessObjectUtils {
+class DBAccessObjectUtils implements IDBAccessObject {
        /**
         * @param integer $bitfield
         * @param integer $flags IDBAccessObject::READ_* constant
@@ -37,23 +37,45 @@ class DBAccessObjectUtils {
        }
 
        /**
-        * Get an appropriate DB index and options for a query
+        * Get an appropriate DB index, options, and fallback DB index for a query
         *
-        * @param integer $bitfield
-        * @return array (DB_MASTER/DB_REPLICA, SELECT options array)
+        * The fallback DB index and options are to be used if the entity is not found
+        * with the initial DB index, typically querying the master DB to avoid lag
+        *
+        * @param integer $bitfield Bitfield of IDBAccessObject::READ_* constants
+        * @return array List of DB indexes and options in this order:
+        *   - DB_MASTER or DB_REPLICA constant for the initial query
+        *   - SELECT options array for the initial query
+        *   - DB_MASTER constant for the fallback query; null if no fallback should happen
+        *   - SELECT options array for the fallback query; empty if no fallback should happen
         */
        public static function getDBOptions( $bitfield ) {
-               $index = self::hasFlags( $bitfield, IDBAccessObject::READ_LATEST )
-                       ? DB_MASTER
-                       : DB_REPLICA;
+               if ( self::hasFlags( $bitfield, self::READ_LATEST_IMMUTABLE ) ) {
+                       $index = DB_REPLICA; // override READ_LATEST if set
+                       $fallbackIndex = DB_MASTER;
+               } elseif ( self::hasFlags( $bitfield, self::READ_LATEST ) ) {
+                       $index = DB_MASTER;
+                       $fallbackIndex = null;
+               } else {
+                       $index = DB_REPLICA;
+                       $fallbackIndex = null;
+               }
+
+               $lockingOptions = [];
+               if ( self::hasFlags( $bitfield, self::READ_EXCLUSIVE ) ) {
+                       $lockingOptions[] = 'FOR UPDATE';
+               } elseif ( self::hasFlags( $bitfield, self::READ_LOCKING ) ) {
+                       $lockingOptions[] = 'LOCK IN SHARE MODE';
+               }
 
-               $options = [];
-               if ( self::hasFlags( $bitfield, IDBAccessObject::READ_EXCLUSIVE ) ) {
-                       $options[] = 'FOR UPDATE';
-               } elseif ( self::hasFlags( $bitfield, IDBAccessObject::READ_LOCKING ) ) {
-                       $options[] = 'LOCK IN SHARE MODE';
+               if ( $fallbackIndex !== null ) {
+                       $options = []; // locks on DB_REPLICA make no sense
+                       $fallbackOptions = $lockingOptions;
+               } else {
+                       $options = $lockingOptions;
+                       $fallbackOptions = []; // no fallback
                }
 
-               return [ $index, $options ];
+               return [ $index, $options, $fallbackIndex, $fallbackOptions ];
        }
 }
index 1019e72..9997f2c 100644 (file)
@@ -469,6 +469,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function flushSnapshot( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        public function listTables( $prefix = null, $fname = __METHOD__ ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
index a011107..ced7379 100644 (file)
@@ -3044,7 +3044,7 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
-       public function clearSnapshot( $fname = __METHOD__ ) {
+       public function flushSnapshot( $fname = __METHOD__ ) {
                if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
                        // This only flushes transactions to clear snapshots, not to write data
                        throw new DBUnexpectedError(
index 9cd95a1..0a178de 100644 (file)
@@ -1588,21 +1588,21 @@ SQL;
         */
        public function lock( $lockName, $method, $timeout = 5 ) {
                $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
-               for ( $attempts = 1; $attempts <= $timeout; ++$attempts ) {
-                       $result = $this->query(
-                               "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
-                       $row = $this->fetchObject( $result );
-                       if ( $row->lockstatus === 't' ) {
-                               parent::lock( $lockName, $method, $timeout ); // record
-                               return true;
-                       } else {
-                               sleep( 1 );
-                       }
-               }
+               $loop = new WaitConditionLoop(
+                       function () use ( $lockName, $key, $timeout, $method ) {
+                               $res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
+                               $row = $this->fetchObject( $res );
+                               if ( $row->lockstatus === 't' ) {
+                                       parent::lock( $lockName, $method, $timeout ); // record
+                                       return true;
+                               }
 
-               wfDebug( __METHOD__ . " failed to acquire lock\n" );
+                               return WaitConditionLoop::CONDITION_CONTINUE;
+                       },
+                       $timeout
+               );
 
-               return false;
+               return ( $loop->invoke() === $loop::CONDITION_REACHED );
        }
 
        /**
index fa24144..f312357 100644 (file)
@@ -1420,7 +1420,7 @@ interface IDatabase {
         * will cause a warning, unless the current transaction was started
         * automatically because of the DBO_TRX flag.
         *
-        * @param string $fname
+        * @param string $fname Calling function name
         * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional]
         * @throws DBError
         */
@@ -1458,7 +1458,7 @@ interface IDatabase {
         * throwing an Exception is preferrable, using a pre-installed error handler to trigger
         * rollback (in any case, failure to issue COMMIT will cause rollback server-side).
         *
-        * @param string $fname
+        * @param string $fname Calling function name
         * @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
         *   constant to disable warnings about calling rollback when no transaction is in
         *   progress. This will silently break any ongoing explicit transaction. Only set the
@@ -1468,6 +1468,20 @@ interface IDatabase {
         */
        public function rollback( $fname = __METHOD__, $flush = '' );
 
+       /**
+        * Commit any transaction but error out if writes or callbacks are pending
+        *
+        * This is intended for clearing out REPEATABLE-READ snapshots so that callers can
+        * see a new point-in-time of the database. This is useful when one of many transaction
+        * rounds finished and significant time will pass in the script's lifetime. It is also
+        * useful to call on a replica DB after waiting on replication to catch up to the master.
+        *
+        * @param string $fname Calling function name
+        * @throws DBUnexpectedError
+        * @since 1.28
+        */
+       public function flushSnapshot( $fname = __METHOD__ );
+
        /**
         * List all tables on the database
         *
index 907cbc8..1f4b993 100644 (file)
@@ -1181,7 +1181,7 @@ class LoadBalancer {
                        function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
                                $conn->setTrxEndCallbackSuppression( true );
                                try {
-                                       $conn->clearSnapshot( $fname );
+                                       $conn->flushSnapshot( $fname );
                                } catch ( DBError $e ) {
                                        MWExceptionHandler::logException( $e );
                                        $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
@@ -1215,7 +1215,7 @@ class LoadBalancer {
                                        if ( $conn->writesOrCallbacksPending() ) {
                                                $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
                                        } elseif ( $restore ) {
-                                               $conn->clearSnapshot( $fname );
+                                               $conn->flushSnapshot( $fname );
                                        }
                                } catch ( DBError $e ) {
                                        MWExceptionHandler::logException( $e );
@@ -1337,7 +1337,7 @@ class LoadBalancer {
         */
        public function flushReplicaSnapshots( $fname = __METHOD__ ) {
                $this->forEachOpenReplicaConnection( function ( DatabaseBase $conn ) {
-                       $conn->clearSnapshot( __METHOD__ );
+                       $conn->flushSnapshot( __METHOD__ );
                } );
        }
 
index 567a298..a3cb3b1 100644 (file)
@@ -103,17 +103,17 @@ abstract class LockManager {
         */
        final public function lockByType( array $pathsByType, $timeout = 0 ) {
                $pathsByType = $this->normalizePathsByType( $pathsByType );
-               $msleep = [ 0, 50, 100, 300, 500 ]; // retry backoff times
-               $start = microtime( true );
-               do {
-                       $status = $this->doLockByType( $pathsByType );
-                       $elapsed = microtime( true ) - $start;
-                       if ( $status->isOK() || $elapsed >= $timeout || $elapsed < 0 ) {
-                               break; // success, timeout, or clock set back
-                       }
-                       usleep( 1e3 * ( next( $msleep ) ?: 1000 ) ); // use 1 sec after enough times
-                       $elapsed = microtime( true ) - $start;
-               } while ( $elapsed < $timeout && $elapsed >= 0 );
+
+               $status = null;
+               $loop = new WaitConditionLoop(
+                       function () use ( &$status, $pathsByType ) {
+                               $status = $this->doLockByType( $pathsByType );
+
+                               return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE;
+                       },
+                       $timeout
+               );
+               $loop->invoke();
 
                return $status;
        }
index cb5266a..2f17e27 100644 (file)
@@ -276,6 +276,7 @@ class MemcLockManager extends QuorumLockManager {
         * @return MemcachedBagOStuff|null
         */
        protected function getCache( $lockSrv ) {
+               /** @var BagOStuff $memc */
                $memc = null;
                if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
                        $memc = $this->bagOStuffs[$lockSrv];
@@ -337,20 +338,21 @@ class MemcLockManager extends QuorumLockManager {
                // Try to quickly loop to acquire the keys, but back off after a few rounds.
                // This reduces memcached spam, especially in the rare case where a server acquires
                // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
-               $rounds = 0;
-               $start = microtime( true );
-               do {
-                       if ( ( ++$rounds % 4 ) == 0 ) {
-                               usleep( 1000 * 50 ); // 50 ms
-                       }
-                       foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
-                               if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
-                                       $lockedKeys[] = $key;
-                               } else {
-                                       continue; // acquire in order
+               $loop = new WaitConditionLoop(
+                       function () use ( $memc, $keys, &$lockedKeys ) {
+                               foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
+                                       if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
+                                               $lockedKeys[] = $key;
+                                       }
                                }
-                       }
-               } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 3 );
+
+                               return array_diff( $keys, $lockedKeys )
+                                       ? WaitConditionLoop::CONDITION_CONTINUE
+                                       : true;
+                       },
+                       3.0 // timeout
+               );
+               $loop->invoke();
 
                if ( count( $lockedKeys ) != count( $keys ) ) {
                        $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
diff --git a/includes/installer/i18n/lij.json b/includes/installer/i18n/lij.json
new file mode 100644 (file)
index 0000000..54c2c9e
--- /dev/null
@@ -0,0 +1,24 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Giromin Cangiaxo"
+               ]
+       },
+       "config-desc": "Programma de installaçion pe MediaWiki",
+       "config-title": "Installaçion de MediaWiki $1",
+       "config-information": "Informaçioin",
+       "config-localsettings-upgrade": "L'è stæto rilevou un file <code>LocalSettings.php</code>.\nPe aggiornâ questa installaçion, se prega de insei o valô de <code>$wgUpgradeKey</code> inta casella chì de sotta.\nO poei atrovâ in <code>LocalSettings.php</code>.",
+       "config-localsettings-cli-upgrade": "L 'è stæto rilevou un file <code>LocalSettings.php</code>.\nPe aggiornâ questa installaçion, eseguî <code>update.php</code>",
+       "config-localsettings-key": "Ciave de aggiornamento:",
+       "config-localsettings-badkey": "A ciave d'agiornamento che t'hæ fornio a no l'è corretta.",
+       "config-upgrade-key-missing": "L'è stæto rilevou un'installaçion existente de MediaWiki.\nPe aggiornâ quest'installaçion, se prega de insei a seguente riga inta parte infeiô do to <code>LocalSettings.php</code>:\n\n$1",
+       "config-localsettings-incomplete": "O file <code>LocalSettings.php</code> existente pâ ese incompleto.\nA variabile $1 a no l'è impostâ.\nCangia <code>LocalSettings.php</code> de moddo che questa variabile a segge impostâ e fanni clic insce \"{{int:Config-continue}}\".",
+       "config-localsettings-connection-error": "S'è veificou un errô durante a connescion a-o database doeuviando e impostaçioin specificæ in <code>LocalSettings.php</code>. Se prega de coreze queste impostaçioin e riprovâ.\n\n$1",
+       "config-session-error": "Errô inte l'avvio da sescion: $1",
+       "config-session-expired": "I dæti da sescion pan ese descheiti.\nE sescioin son configuæ pe 'na duata de $1.\nTi poeu aomentâla impostando <code>session.gc_maxlifetime</code> into file php.ini.\nRiavvia o processo d'installaçion.",
+       "config-no-session": "I dæti da sescion son andæti persci!\nControlla o to file php.ini e aseguite che <code>session.save_path</code> o l'è impostou insce 'na directory apropiâ.",
+       "config-your-language": "A to lengua:",
+       "config-your-language-help": "Seleçion-a una lengua da doeuviâ  durante o processo d'installaçion.",
+       "config-wiki-language": "A lengua do wiki:",
+       "config-wiki-language-help": "Seleçion-a a lengua ch'a saiâ prevalentemente doeuviâ into wiki."
+}
index 459910a..d0a7719 100644 (file)
@@ -26,7 +26,7 @@ use Interwiki;
 /**
  * Service interface for looking up Interwiki records.
  *
- * @singe 1.28
+ * @since 1.28
  */
 interface InterwikiLookup {
 
index 7a6b5fc..570d6db 100644 (file)
@@ -537,6 +537,10 @@ class JobRunner implements LoggerAwareInterface {
                        // This will trigger a rollback in the main loop
                        throw new DBError( $dbwSerial, "Timed out waiting on commit queue." );
                }
+               $unlocker = new ScopedCallback( function () use ( $dbwSerial ) {
+                       $dbwSerial->unlock( 'jobrunner-serial-commit', __METHOD__ );
+               } );
+
                // Wait for the replica DBs to catch up
                $pos = $lb->getMasterPos();
                if ( $pos ) {
@@ -545,8 +549,6 @@ class JobRunner implements LoggerAwareInterface {
 
                // Actually commit the DB master changes
                $lbFactory->commitMasterChanges( $fnameTrxOwner );
-
-               // Release the lock
-               $dbwSerial->unlock( 'jobrunner-serial-commit', __METHOD__ );
+               ScopedCallback::consume( $unlocker );
        }
 }
index e6f59f3..1828dd7 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * Job to add recent change entries mentioning category membership changes
@@ -48,7 +49,8 @@ class CategoryMembershipChangeJob extends Job {
                        return false; // deleted?
                }
 
-               $dbw = wfGetDB( DB_MASTER );
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $dbw = $lb->getConnection( DB_MASTER );
                // Use a named lock so that jobs for this page see each others' changes
                $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
                $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
@@ -59,12 +61,12 @@ class CategoryMembershipChangeJob extends Job {
 
                $dbr = wfGetDB( DB_REPLICA, [ 'recentchanges' ] );
                // Wait till the replica DB is caught up so that jobs for this page see each others' changes
-               if ( !wfGetLB()->safeWaitForMasterPos( $dbr ) ) {
+               if ( !$lb->safeWaitForMasterPos( $dbr ) ) {
                        $this->setLastError( "Timed out while waiting for replica DB to catch up" );
                        return false;
                }
                // Clear any stale REPEATABLE-READ snapshot
-               wfGetLBFactory()->commitAll( __METHOD__ );
+               $dbr->flushSnapshot( __METHOD__ );
 
                $cutoffUnix = wfTimestamp( TS_UNIX, $this->params['revTimestamp'] );
                // Using ENQUEUE_FUDGE_SEC handles jobs inserted out of revision order due to the delay
@@ -118,19 +120,23 @@ class CategoryMembershipChangeJob extends Job {
                );
 
                // Apply all category updates in revision timestamp order
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                foreach ( $res as $row ) {
-                       $this->notifyUpdatesForRevision( $page, Revision::newFromRow( $row ) );
+                       $this->notifyUpdatesForRevision( $lbFactory, $page, Revision::newFromRow( $row ) );
                }
 
                return true;
        }
 
        /**
+        * @param LBFactory $lbFactory
         * @param WikiPage $page
         * @param Revision $newRev
         * @throws MWException
         */
-       protected function notifyUpdatesForRevision( WikiPage $page, Revision $newRev ) {
+       protected function notifyUpdatesForRevision(
+               LBFactory $lbFactory, WikiPage $page, Revision $newRev
+       ) {
                $config = RequestContext::getMain()->getConfig();
                $title = $page->getTitle();
 
@@ -156,9 +162,7 @@ class CategoryMembershipChangeJob extends Job {
                        return; // nothing to do
                }
 
-               $dbw = wfGetDB( DB_MASTER );
-               $factory = wfGetLBFactory();
-               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+               $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
 
                $catMembChange = new CategoryMembershipChange( $title, $newRev );
                $catMembChange->checkTemplateLinks();
@@ -170,7 +174,7 @@ class CategoryMembershipChangeJob extends Job {
                        $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
                        $catMembChange->triggerCategoryAddedNotification( $categoryTitle );
                        if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) {
-                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+                               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
                        }
                }
 
@@ -178,7 +182,7 @@ class CategoryMembershipChangeJob extends Job {
                        $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
                        $catMembChange->triggerCategoryRemovedNotification( $categoryTitle );
                        if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) {
-                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+                               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
                        }
                }
        }
index 3cc6236..3339eb3 100644 (file)
@@ -37,8 +37,9 @@ class WaitConditionLoop {
 
        const CONDITION_REACHED = 1;
        const CONDITION_CONTINUE = 0; // evaluates as falsey
-       const CONDITION_TIMED_OUT = -1;
-       const CONDITION_ABORTED = -2;
+       const CONDITION_FAILED = -1;
+       const CONDITION_TIMED_OUT = -2;
+       const CONDITION_ABORTED = -3;
 
        /**
         * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
@@ -53,11 +54,14 @@ class WaitConditionLoop {
 
        /**
         * Invoke the loop and continue until either:
-        *   - a) The condition callback does not return either CONDITION_CONTINUE or true
+        *   - a) The condition callback returns neither CONDITION_CONTINUE nor false
         *   - b) The timeout is reached
         * This a condition callback can return true (stop) or false (continue) for convenience.
         * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
         *
+        * If $timeout is 0, then only the condition callback will be called (no busy callbacks),
+        * and this will immediately return CONDITION_FAILED if the condition was not met.
+        *
         * Exceptions in callbacks will be caught and the callback will be swapped with
         * one that simply rethrows that exception back to the caller when invoked.
         *
@@ -77,12 +81,19 @@ class WaitConditionLoop {
                        $checkResult = call_user_func( $this->condition );
                        $cpu = $this->getCpuTime() - $cpuStart;
                        $real = $this->getWallTime() - $realStart;
-                       // Exit if the condition is reached
-                       if ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
-                               $finalResult = is_int( $checkResult ) ? $checkResult : self::CONDITION_REACHED;
+                       // Exit if the condition is reached, and error occurs, or this is non-blocking
+                       if ( $this->timeout <= 0 ) {
+                               $finalResult = $checkResult ? self::CONDITION_REACHED : self::CONDITION_FAILED;
+                               break;
+                       } elseif ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
+                               if ( is_int( $checkResult ) ) {
+                                       $finalResult = $checkResult;
+                               } else {
+                                       $finalResult = self::CONDITION_REACHED;
+                               }
                                break;
                        } elseif ( $lastCheck ) {
-                               break; // timeout
+                               break; // timeout reached
                        }
                        // Detect if condition callback seems to block or if justs burns CPU
                        $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
index a679be8..cd79b67 100644 (file)
@@ -410,35 +410,21 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                }
 
                $expiry = min( $expiry ?: INF, self::TTL_DAY );
-
-               $this->clearLastError();
-               $timestamp = microtime( true ); // starting UNIX timestamp
-               if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
-                       $locked = true;
-               } elseif ( $this->getLastError() || $timeout <= 0 ) {
-                       $locked = false; // network partition or non-blocking
-               } else {
-                       // Estimate the RTT (us); use 1ms minimum for sanity
-                       $uRTT = max( 1e3, ceil( 1e6 * ( microtime( true ) - $timestamp ) ) );
-                       $sleep = 2 * $uRTT; // rough time to do get()+set()
-
-                       $attempts = 0; // failed attempts
-                       do {
-                               if ( ++$attempts >= 3 && $sleep <= 5e5 ) {
-                                       // Exponentially back off after failed attempts to avoid network spam.
-                                       // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts.
-                                       $sleep *= 2;
-                               }
-                               usleep( $sleep ); // back off
+               $loop = new WaitConditionLoop(
+                       function () use ( $key, $timeout, $expiry ) {
                                $this->clearLastError();
-                               $locked = $this->add( "{$key}:lock", 1, $expiry );
-                               if ( $this->getLastError() ) {
-                                       $locked = false; // network partition
-                                       break;
+                               if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
+                                       return true; // locked!
+                               } elseif ( $this->getLastError() ) {
+                                       return WaitConditionLoop::CONDITION_ABORTED; // network partition?
                                }
-                       } while ( !$locked && ( microtime( true ) - $timestamp ) < $timeout );
-               }
 
+                               return WaitConditionLoop::CONDITION_CONTINUE;
+                       },
+                       $timeout
+               );
+
+               $locked = ( $loop->invoke() === $loop::CONDITION_REACHED );
                if ( $locked ) {
                        $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
                }
index af78f43..adb2899 100644 (file)
@@ -121,6 +121,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        const TTL_LAGGED = 30;
        /** Idiom for delete() for "no hold-off" */
        const HOLDOFF_NONE = 0;
+       /** Idiom for getWithSetCallback() for "no minimum required as-of timestamp" */
+       const MIN_TIMESTAMP_NONE = 0.0;
 
        /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
        const TINY_NEGATIVE = -0.000001;
@@ -385,22 +387,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param integer $ttl Seconds to live. Special values are:
         *   - WANObjectCache::TTL_INDEFINITE: Cache forever
         * @param array $opts Options map:
-        *   - lag     : Seconds of replica DB lag. Typically, this is either the replica DB lag
-        *               before the data was read or, if applicable, the replica DB lag before
-        *               the snapshot-isolated transaction the data was read from started.
-        *               Default: 0 seconds
-        *   - since   : UNIX timestamp of the data in $value. Typically, this is either
-        *               the current time the data was read or (if applicable) the time when
-        *               the snapshot-isolated transaction the data was read from started.
-        *               Default: 0 seconds
+        *   - lag : Seconds of replica DB lag. Typically, this is either the replica DB lag
+        *      before the data was read or, if applicable, the replica DB lag before
+        *      the snapshot-isolated transaction the data was read from started.
+        *      Use false to indicate that replication is not running.
+        *      Default: 0 seconds
+        *   - since : UNIX timestamp of the data in $value. Typically, this is either
+        *      the current time the data was read or (if applicable) the time when
+        *      the snapshot-isolated transaction the data was read from started.
+        *      Default: 0 seconds
         *   - pending : Whether this data is possibly from an uncommitted write transaction.
-        *               Generally, other threads should not see values from the future and
-        *               they certainly should not see ones that ended up getting rolled back.
-        *               Default: false
+        *      Generally, other threads should not see values from the future and
+        *      they certainly should not see ones that ended up getting rolled back.
+        *      Default: false
         *   - lockTSE : if excessive replication/snapshot lag is detected, then store the value
-        *               with this TTL and flag it as stale. This is only useful if the reads for
-        *               this key use getWithSetCallback() with "lockTSE" set.
-        *               Default: WANObjectCache::TSE_NONE
+        *      with this TTL and flag it as stale. This is only useful if the reads for
+        *      this key use getWithSetCallback() with "lockTSE" set.
+        *      Default: WANObjectCache::TSE_NONE
         * @return bool Success
         */
        final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
@@ -643,6 +646,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *   - $oldValue : current cache value or false if not present
         *   - &$ttl : a reference to the TTL which can be altered
         *   - &$setOpts : a reference to options for set() which can be altered
+        *   - $oldAsOf : generation UNIX timestamp of $oldValue or null if not present (since 1.28)
         *
         * It is strongly recommended to set the 'lag' and 'since' fields to avoid race conditions
         * that can cause stale values to get stuck at keys. Usually, callbacks ignore the current
@@ -793,8 +797,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *      isolation anyway, this should not typically matter.
         *      Default: WANObjectCache::TTL_UNCACHEABLE.
         *   - pcGroup: Process cache group to use instead of the primary one. If set, this must be
-        *      of the format <alphanumeric name>:<max key size>. Use this for storing large values,
-        *      small but numerous values, or a few values with a high cost if they are evicted.
+        *      of the format ALPHANUMERIC_NAME:MAX_KEY_SIZE, e.g. "mydata:10". Use this for storing
+        *      large values, small yet numerous values, or some values with a high cost of eviction.
         *      It is generally preferable to use a class constant when setting this value.
         *      This has no effect unless pcTTL is used.
         *      Default: WANObjectCache::PC_PRIMARY.
@@ -803,6 +807,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *      versions are stored alongside older versions concurrently. Avoid storing class objects
         *      however, as this reduces compatibility (due to serialization).
         *      Default: null.
+        *   - minAsOf: Reject values if they were generated before this UNIX timestamp.
+        *      This is useful if the source of a key is suspected of having possibly changed
+        *      recently, and the caller wants any such changes to be reflected.
+        *      Default: WANObjectCache::MIN_TIMESTAMP_NONE.
         *   - hotTTR: Expected time-till-refresh for keys that average ~1 hit/second.
         *      This should be greater than "ageNew". Keys with higher hit rates will regenerate
         *      more often. This is useful when a popular key is changed but the cache purge was
@@ -831,8 +839,6 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                }
 
                if ( $value === false ) {
-                       unset( $opts['minTime'] ); // not a public feature
-
                        // Fetch the value over the network
                        if ( isset( $opts['version'] ) ) {
                                $version = $opts['version'];
@@ -840,7 +846,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                $cur = $this->doGetWithSetCallback(
                                        $key,
                                        $ttl,
-                                       function ( $oldValue, &$ttl, &$setOpts ) use ( $callback, $version ) {
+                                       function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
+                                       use ( $callback, $version ) {
                                                if ( is_array( $oldValue )
                                                        && array_key_exists( self::VFLD_DATA, $oldValue )
                                                ) {
@@ -851,7 +858,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                                }
 
                                                return [
-                                                       self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts ),
+                                                       self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
                                                        self::VFLD_VERSION => $version
                                                ];
                                        },
@@ -869,7 +876,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                                $ttl,
                                                $callback,
                                                // Regenerate value if not newer than $key
-                                               [ 'version' => null, 'minTime' => $asOf ] + $opts
+                                               [ 'version' => null, 'minAsOf' => $asOf ] + $opts
                                        );
                                }
                        } else {
@@ -893,8 +900,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param string $key
         * @param integer $ttl
         * @param callback $callback
-        * @param array $opts Options map for getWithSetCallback() which also includes:
-        *   - minTime: Treat values older than this UNIX timestamp as not existing. Default: null.
+        * @param array $opts Options map for getWithSetCallback()
         * @param float &$asOf Cache generation timestamp of returned value [returned]
         * @return mixed
         * @note Callable type hints are not used to avoid class-autoloading
@@ -906,7 +912,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
                $popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
                $ageNew = isset( $opts['ageNew'] ) ? $opts['ageNew'] : self::AGE_NEW;
-               $minTime = isset( $opts['minTime'] ) ? $opts['minTime'] : 0.0;
+               $minTime = isset( $opts['minAsOf'] ) ? $opts['minAsOf'] : self::MIN_TIMESTAMP_NONE;
                $versioned = isset( $opts['version'] );
 
                // Get the current key value
@@ -969,13 +975,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                // Generate the new value from the callback...
                $setOpts = [];
-               $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts ] );
-               $asOf = microtime( true );
+               $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
                // When delete() is called, writes are write-holed by the tombstone,
                // so use a special INTERIM key to pass the new value around threads.
                if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
                        $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
-                       $wrapped = $this->wrap( $value, $tempTTL, $asOf );
+                       $newAsOf = microtime( true );
+                       $wrapped = $this->wrap( $value, $tempTTL, $newAsOf );
                        // Avoid using set() to avoid pointless mcrouter broadcasting
                        $this->cache->merge(
                                self::INTERIM_KEY_PREFIX . $key,
index f1e59de..d5dfd3d 100644 (file)
@@ -3235,9 +3235,12 @@ class WikiPage implements Page, IDBAccessObject {
                        $flags |= EDIT_FORCE_BOT;
                }
 
+               $targetContent = $target->getContent();
+               $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
+
                // Actually store the edit
                $status = $this->doEditContent(
-                       $target->getContent(),
+                       $targetContent,
                        $summary,
                        $flags,
                        $target->getId(),
@@ -3287,6 +3290,22 @@ class WikiPage implements Page, IDBAccessObject {
                        ] ];
                }
 
+               if ( $changingContentModel ) {
+                       // If the content model changed during the rollback,
+                       // make sure it gets logged to Special:Log/contentmodel
+                       $log = new ManualLogEntry( 'contentmodel', 'change' );
+                       $log->setPerformer( $guser );
+                       $log->setTarget( $this->mTitle );
+                       $log->setComment( $summary );
+                       $log->setParameters( [
+                               '4::oldmodel' => $current->getContentModel(),
+                               '5::newmodel' => $targetContent->getModel(),
+                       ] );
+
+                       $logId = $log->insert( $dbw );
+                       $log->publish( $logId );
+               }
+
                $revId = $statusRev->getId();
 
                Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
index 3cb30bc..ad1ed49 100644 (file)
@@ -140,6 +140,9 @@ class ResourceLoader implements LoggerAwareInterface {
                        }
                }
 
+               // Batched version of ResourceLoaderWikiModule::getTitleInfo
+               ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
+
                // Prime in-object cache for message blobs for modules with messages
                $modules = [];
                foreach ( $moduleNames as $name ) {
index 390f785..5580306 100644 (file)
@@ -246,7 +246,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                $summary = parent::getDefinitionSummary( $context );
                $summary[] = [
                        'pages' => $this->getPages( $context ),
-                       // Includes SHA1 of content
+                       // Includes meta data of current revisions
                        'titleInfo' => $this->getTitleInfo( $context ),
                ];
                return $summary;
@@ -262,7 +262,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                // For user modules, don't needlessly load if there are no non-empty pages
                if ( $this->getGroup() === 'user' ) {
                        foreach ( $revisions as $revision ) {
-                               if ( $revision['rev_len'] > 0 ) {
+                               if ( $revision['page_len'] > 0 ) {
                                        // At least one non-empty page, module should be loaded
                                        return false;
                                }
@@ -276,10 +276,14 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                return count( $revisions ) === 0;
        }
 
+       private function setTitleInfo( $key, array $titleInfo ) {
+               $this->titleInfo[$key] = $titleInfo;
+       }
+
        /**
         * Get the information about the wiki pages for a given context.
         * @param ResourceLoaderContext $context
-        * @return array Keyed by page name. Contains arrays with 'rev_len' and 'rev_sha1' keys
+        * @return array Keyed by page name
         */
        protected function getTitleInfo( ResourceLoaderContext $context ) {
                $dbr = $this->getDB();
@@ -288,37 +292,77 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                        return [];
                }
 
-               $pages = $this->getPages( $context );
-               $key = implode( '|', array_keys( $pages ) );
+               $pageNames = array_keys( $this->getPages( $context ) );
+               sort( $pageNames );
+               $key = implode( '|', $pageNames );
                if ( !isset( $this->titleInfo[$key] ) ) {
-                       $this->titleInfo[$key] = [];
-                       $batch = new LinkBatch;
-                       foreach ( $pages as $titleText => $options ) {
-                               $batch->addObj( Title::newFromText( $titleText ) );
+                       $this->titleInfo[$key] = self::fetchTitleInfo( $dbr, $pageNames, __METHOD__ );
+               }
+               return $this->titleInfo[$key];
+       }
+
+       private static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) {
+               $titleInfo = [];
+               $batch = new LinkBatch;
+               foreach ( $pages as $titleText ) {
+                       $batch->addObj( Title::newFromText( $titleText ) );
+               }
+               if ( !$batch->isEmpty() ) {
+                       $res = $db->select( 'page',
+                               // Include page_touched to allow purging if cache is poisoned (T117587, T113916)
+                               [ 'page_namespace', 'page_title', 'page_touched', 'page_len', 'page_latest' ],
+                               $batch->constructSet( 'page', $db ),
+                               $fname
+                       );
+                       foreach ( $res as $row ) {
+                               // Avoid including ids or timestamps of revision/page tables so
+                               // that versions are not wasted
+                               $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               $titleInfo[$title->getPrefixedText()] = [
+                                       'page_len' => $row->page_len,
+                                       'page_latest' => $row->page_latest,
+                                       'page_touched' => $row->page_touched,
+                               ];
                        }
+               }
+               return $titleInfo;
+       }
 
-                       if ( !$batch->isEmpty() ) {
-                               $res = $dbr->select( [ 'page', 'revision' ],
-                                       // Include page_touched to allow purging if cache is poisoned (T117587, T113916)
-                                       [ 'page_namespace', 'page_title', 'page_touched', 'rev_len', 'rev_sha1' ],
-                                       $batch->constructSet( 'page', $dbr ),
-                                       __METHOD__,
-                                       [],
-                                       [ 'revision' => [ 'INNER JOIN', [ 'page_latest=rev_id' ] ] ]
-                               );
-                               foreach ( $res as $row ) {
-                                       // Avoid including ids or timestamps of revision/page tables so
-                                       // that versions are not wasted
-                                       $title = Title::makeTitle( $row->page_namespace, $row->page_title );
-                                       $this->titleInfo[$key][$title->getPrefixedText()] = [
-                                               'rev_len' => $row->rev_len,
-                                               'rev_sha1' => $row->rev_sha1,
-                                               'page_touched' => $row->page_touched,
-                                       ];
+       /**
+        * @since 1.28
+        * @param ResourceLoaderContext $context
+        * @param IDatabase $db
+        * @param string[] $modules
+        */
+       public static function preloadTitleInfo(
+               ResourceLoaderContext $context, IDatabase $db, array $moduleNames
+       ) {
+               $rl = $context->getResourceLoader();
+               // getDB() can be overridden to point to a foreign database.
+               // For now, only preload local. In the future, we could preload by wikiID.
+               $allPages = [];
+               $wikiModules = [];
+               foreach ( $moduleNames as $name ) {
+                       $module = $rl->getModule( $name );
+                       if ( $module instanceof self ) {
+                               $mDB = $module->getDB();
+                               // Subclasses may disable getDB and implement getTitleInfo differently
+                               if ( $mDB && $mDB->getWikiID() === $db->getWikiID() ) {
+                                       $wikiModules[] = $module;
+                                       $allPages += $module->getPages( $context );
                                }
                        }
                }
-               return $this->titleInfo[$key];
+               $allInfo = self::fetchTitleInfo( $db, array_keys( $allPages ), __METHOD__ );
+               foreach ( $wikiModules as $module ) {
+                       $pages = $module->getPages( $context );
+                       $info = array_intersect_key( $allInfo, $pages );
+                       $pageNames = array_keys( $pages );
+                       sort( $pageNames );
+                       $key = implode( '|', $pageNames );
+                       $module->setTitleInfo( $key, $info );
+               }
+               return $allInfo;
        }
 
        /**
index ccbb275..b37c475 100644 (file)
@@ -191,6 +191,12 @@ class SpecialChangeContentModel extends FormSpecialPage {
                        // Page doesn't exist, create an empty content object
                        $newContent = ContentHandler::getForModelID( $data['model'] )->makeEmptyContent();
                }
+
+               // All other checks have passed, let's check rate limits
+               if ( $user->pingLimiter( 'editcontentmodel' ) ) {
+                       throw new ThrottledError();
+               }
+
                $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW;
                $flags |= EDIT_INTERNAL;
                if ( $user->isAllowed( 'bot' ) ) {
index 4576928..ee5b633 100644 (file)
        "file-thumbnail-no": "Назва файла пачынаецца з <strong>$1</strong>.\nВерагодна гэта паменшаная копія выявы ''(мініятура)''.\nКалі Вы маеце гэтую выяву ў поўным памеры, загрузіце яе, альбо зьмяніце назву файла.",
        "fileexists-forbidden": "Файл з такой назвай ужо існуе і ня можа быць перапісаны.\nКалі ласка, вярніцеся назад і загрузіце гэты файл з новай назвай. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Файл з такой назвай ужо існуе ў агульным сховішчы файлаў.\nКалі Вы жадаеце загрузіць Ваш файл, вярніцеся назад і загрузіце гэты файл з новай назвай. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Гэтая загрузка зьяўляецца дакладнай копіяй цяперашняй вэрсіі <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Гэты файл дублюе {{PLURAL:$1|1=наступны файл|наступныя файлы}}:",
        "file-deleted-duplicate": "Падобны файл ([[:$1]]) ужо выдаляўся. Калі ласка, паглядзіце гісторыю выдаленьняў гэтага файла перад яго паўторнай загрузкай.",
        "file-deleted-duplicate-notitle": "Файл, ідэнтычны гэтаму файлу, раней ужо быў выдалены, а назва файла была забароненая.\nВам трэба зьвярнуцца да некага з правамі прагляду зьвестак забароненых файлаў, каб прааналізаваць сытуацыю перад тым, як загружаць файл ізноў.",
index 254ac57..a30b31a 100644 (file)
        "grant-group-high-volume": "Выконваць вялікі аб'ём дзейнасці",
        "grant-group-customization": "Настройкі і перавагі",
        "grant-group-administration": "Выконваць адміністрацыйныя дзеянні",
+       "grant-group-private-information": "Доступ да прыватных звестак пра вас",
        "grant-group-other": "Розная актыўнасць",
        "grant-blockusers": "Блакаваць і разблакаваць удзельнікаў",
        "grant-createaccount": "Ствараць уліковыя запісы",
        "grant-highvolume": "Вялікі аб'ём рэдагавання",
        "grant-oversight": "Утойваць удзельнікаў і версіі старонак",
        "grant-patrol": "Патруляваць змены старонак",
+       "grant-privateinfo": "Доступ да прыватных звестак",
        "grant-protect": "Ахоўваць і здымаць ахову старонак",
        "grant-rollback": "Адкатваць змяненні старонак",
        "grant-sendemail": "Адпраўляць электронную пошту іншым удзельнікам",
        "rightslogtext": "Журнал змяненняў у дазволах, прыпісаных удзельнікам.",
        "action-read": "чытаць гэтую старонку",
        "action-edit": "правіць гэтую старонку",
-       "action-createpage": "ствараць старонкі",
-       "action-createtalk": "ствараць размоўныя старонкі",
+       "action-createpage": "стварыць гэту старонку",
+       "action-createtalk": "стварыць гэту размоўную старонку",
        "action-createaccount": "ствараць гэты рахунак удзельніка",
        "action-autocreateaccount": "аўтаматычна ствараць гэты вонкавы ўліковы запіс удзельніка",
        "action-history": "глядзець гісторыю гэтай старонкі",
        "action-applychangetags": "прымяняць біркі з сваімі праўкамі",
        "action-changetags": "дадаваць і выдаляць адвольныя біркі да асобных версій і запісаў у журнале падзей",
        "action-deletechangetags": "выдаляць біркі з базы даных",
+       "action-purge": "ачысціць кэш гэтай старонкі",
        "nchanges": "$1 {{PLURAL:$1|змена|змены|змен}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з часу апошняга наведвання}}",
        "enhancedrc-history": "гісторыя",
        "file-thumbnail-no": "Назва файла пачынаецца з <strong>$1</strong>.\nТак можа называцца выява зменшанага памеру ''(драбніца)''.\nКалі гэтая выява сапраўды запісаная ў найлепшым разрозненні, якое ёсць, то ўкладайце яе, а іначай лепей памяняць назву файла.",
        "fileexists-forbidden": "Файл з такой назвай ужо ёсць, і нельга запісаць паўзверх яго. Калі вы жадаеце абавязкова ўкласці свой файл, то выберыце новую назву. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "У агульным сховішчы ўжо існуе файл з такою назвай.\nКалі вы жадаеце ўсё ж укласці свой файл, паўтарыце працэдуру ўкладання, але з іншай назвай. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Укладанне з'яўляецца дакладнай копіяй бягучай версіі <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "Укладанне з'яўляецца дакладнай копіяй {{PLURAL:$2|старой версіі|старых версій}} файла <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Гэты файл з'яўляецца дублікатам наступн{{PLURAL:$1|ага файла|ых файлаў}}:",
        "file-deleted-duplicate": "Файл, падобны да гэтага ([[:$1]]), быў сцёрты некалі раней. Трэба праверыць гісторыю таго файла перад тым, як укладваць яго нанова.",
        "file-deleted-duplicate-notitle": "Файл, ідэнтычны гэтаму, быў сцёрты раней, а назва файла заглушана.\nВы мусіце спытаць каго-небудзь з магчымасцю бачыць заглушаныя звесткі па файлах пераглядзець сітуацыю перад тым, як укладваць яго нанова.",
        "htmlform-cloner-delete": "Сцерці",
        "htmlform-cloner-required": "Неабходна хаця б адно значэнне.",
        "htmlform-title-badnamespace": "[[:$1]] не ў прасторы назваў \"{{ns:$2}}\".",
+       "htmlform-title-not-creatable": "\"$1\" - немагчымы загаловак для старонкі",
        "htmlform-title-not-exists": "$1 не існуе.",
        "htmlform-user-not-exists": "<strong>$1</strong> не існуе.",
+       "htmlform-user-not-valid": "<strong>$1</strong> - недапушчальная назва уліковага запісу.",
        "sqlite-has-fts": "$1 з падтрымкай поўна-тэкставага пошуку",
        "sqlite-no-fts": "$1 без падтрымкі поўна-тэкставага пошуку",
        "logentry-delete-delete": "$1 {{GENDER:$2|сцёр|сцёрла}} старонку $3",
        "logentry-block-block": "$1 заблакірава{{GENDER:$2|ў|ла}} {{GENDER:$4|$3}} на перыяд $5 $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|разблакаваў|разблакавала}} {{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1 {{GENDER:$2|памяняў|памяняла}} настройкі блакіроўкі {{GENDER:$4|$3}} на перыяд $5 $6",
+       "logentry-suppress-block": "$1 {{GENDER:$2|заблакіраваў|заблакіравала}} {{GENDER:$4|$3}} на перыяд $5 $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|памяняў|памяняла}} параметры блакіроўкі {{GENDER:$4|$3}} на перыяд $5 $6",
+       "logentry-import-upload": "$1 {{GENDER:$2|імпартаваў|імпартавала}} $3 праз укладанне файла",
+       "logentry-import-upload-details": "$1 {{GENDER:$2|імпартаваў|імпартавала}} $3 праз укладанне файла ($4 {{PLURAL:$4|версія|версіі|версій}})",
        "logentry-import-interwiki": "$1 {{GENDER:$2|імпартаваў|імпартавала}} $3 з іншай вікі",
+       "logentry-import-interwiki-details": "$1 {{GENDER:$2|імпартаваў|імпартавала}} $3 з $5 ($4 {{PLURAL:$4|версія|версіі|версій}})",
        "logentry-move-move": "$1 {{GENDER:$2|перанёс|перанесла}} старонку $3 у $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|перанёс|перанесла}} старонку $3 у $4, не пакінуўшы перасылкі",
        "logentry-move-move_redir": "$1 {{GENDER:$2|перанёс|перанесла}} старонку $3 у $4 па-над перасылкаю",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|уклаў|уклала}} новую версію $3",
        "logentry-upload-revert": "$1 {{GENDER:$2|уклаў|уклала}} $3",
        "log-name-managetags": "Журнал кіравання біркамі",
+       "logentry-managetags-create": "$1 {{GENDER:$2|стварыў|стварыла}} бірку \"$4\"",
+       "logentry-managetags-delete": "$1 {{GENDER:$2|выдаліў|выдаліла}} бірку \"$4\" (выдалена з $5 {{PLURAL:$5|версіі ці запісу ў журнале|версій і/або запісаў у журнале}})",
+       "logentry-managetags-activate": "$1 {{GENDER:$2|актываваў|актывавала}} бірку \"$4\" для выкарыстання ўдзельнікамі і робатамі",
+       "logentry-managetags-deactivate": "$1 {{GENDER:$2|дэактываваў|дэактывавала}} бірку \"$4\" для выкарыстання ўдзельнікамі і робатамі",
        "log-name-tag": "Журнал бірак",
+       "log-description-tag": "На гэтай старонцы паказана, калі ўдзельнікі дадавалі ці выдалялі [[Special:Tags|біркі]] ў асобных версіях ці запісах журнала. Журнал не захоўвае дзеянні з біркамі, калі яны былі часткай рэдагавання, выдалення ці падобных дзеянняў.",
+       "logentry-tag-update-add-revision": "$1 {{GENDER:$2|дадаў|дадала}} {{PLURAL:$7|1=бірку|біркі}} $6 да версіі $4 старонкі $3",
+       "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|дадаў|дадала}} {{PLURAL:$7|1=бірку|біркі}} $6 да запісу ў журнале $5 старонкі $3",
+       "logentry-tag-update-remove-revision": "$1 {{GENDER:$2|выдаліў|выдаліла}} {{PLURAL:$9|1=бірку|біркі}} $8 з версіі $4 старонкі $3",
        "rightsnone": "(няма)",
        "revdelete-summary": "тлумачэнне праўкі",
        "feedback-adding": "Даданне водгуку на старонку…",
index 02bd9ba..d87a915 100644 (file)
@@ -28,7 +28,8 @@
                        "Sayma Jahan",
                        "Macofe",
                        "Bodhisattwa",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "আজিজ"
                ]
        },
        "tog-underline": "সংযোগগুলির নিচে দাগ দেখানো হোক:",
        "action-createpage": "এই পাতাটি তৈরি করার",
        "action-createtalk": "এই আলাপ পাতাটি তৈরি করার",
        "action-createaccount": "এই ব্যবহারকারী একাউন্টটি তৈরি করার",
+       "action-autocreateaccount": "স্বয়ংক্রিয়ভাবে এই বাহ্যিক ব্যবহারকারী অ্যাকাউন্ট তৈরি করার",
        "action-history": "এই পাতার ইতিহাস দেখার",
        "action-minoredit": "এই সম্পাদনাটি অনুল্লেখ্য হিসেবে চিহ্নিত করার",
        "action-move": "পাতাটি সরিয়ে ফেলুন",
        "action-managechangetags": "ট্যাগ তৈরি ও সক্রিয়/নিষ্ক্রিয়",
        "action-applychangetags": "আপনার পরিবর্তনগুলোর সাথে ট্যাগ সংযোজন করুন",
        "action-changetags": "নির্দিষ্ট সংস্করণ এবং দীর্ঘ সম্পাদনাগুলোতে ট্যাগ সংযোজন ও অপসারণ করুন",
+       "action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার",
        "action-purge": "এই পাতা হালনাগাদ করার",
        "nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি",
        "file-thumbnail-no": "ফাইলের নামটি <strong>$1</strong> দিয়ে শুরু হয়েছে।\nমনে হচ্ছে এটি একটি সংকুচিত আকারের ছবি  ''(থাম্বনেইল)''।\nআপনার কাছে যদি পূর্ণ রেজোলিউশনের ছবিটি থাকে, তবে সেটি আপলোড করুন, নতুবা অনুগ্রহ করে ফাইলের নামটি পরিবর্তন করুন।",
        "fileexists-forbidden": "এই নামের একটি ফাইল ইতিমধ্যেই বিদ্যমান, এবং এটি প্রতিস্থাপনযোগ্য নয়।\nআপনি যদি এখনো ফাইলটি আপলোড করতে চান, তবে অনুগ্রহপূর্বক পেছনে গিয়ে একটি নতুন নামে ফাইলটি আপলোড করুন।\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "অংশীদারী ফাইল ভাণ্ডারে এই নামের একটি ফাইল ইতিমধ্যেই বিদ্যমান।\nআপনি যদি এখনো ফাইলটি আপলোড করতে চান, তবে অনুগ্রহপূর্বক পেছনে গিয়ে একটি নতুন নামে ফাইলটি আপলোড করুন।[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "আপলোডটি <strong>[[:$1]]</strong>-এর বর্তমান সংস্করণের হুবহু প্রতিলিপি।",
+       "fileexists-duplicate-version": "এই আপলোডটি <strong>[[:$1]]</strong>-এর একটি {{PLURAL:$2|পুরনো সংস্করণের}} হুবহু প্রতিলিপি।",
        "file-exists-duplicate": "এই ফাইলটি নিচের {{PLURAL:$1|ফাইল|ফাইলগুলির}} অনুলিপি:",
        "file-deleted-duplicate": "এই ফাইলটির মত একটি ফাইল ([[:$1]]) পূর্বে অপসারণ করা হয়েছে।\nপুনরায় আপলোড করার পূর্বে আপনার উচিত আগের ফাইলটির অপসারণের কারণ জানা।",
        "uploadwarning": "আপলোড সতর্কবাণী",
        "credentialsform-provider": "পরিচয়পত্রের ধরন:",
        "credentialsform-account": "অ্যাকাউন্টের নাম:",
        "linkaccounts": "অ্যাকাউন্ট সংযোগ করুন",
-       "linkaccounts-submit": "à¦\85à§\8dযাà¦\95াà¦\89নà§\8dà¦\9f à¦¸à¦\82যà§\8bà¦\97 করুন",
+       "linkaccounts-submit": "à¦\85à§\8dযাà¦\95াà¦\89নà§\8dà¦\9f à¦¸à¦\82যà§\81à¦\95à§\8dত করুন",
        "unlinkaccounts": "অ্যাকাউন্ট সংযোগ বিচ্ছিন্ন করুন",
        "unlinkaccounts-success": "অ্যাকাউন্টের সংযোগ বিচ্ছিন্ন করা হয়েছে।",
        "userjsispublic": "অনুগ্রহ করে লক্ষ্য করুন: জাভাস্ক্রিপ্টের উপপাতাগুলিতে গোপনীয় তথ্য থাকা উচিত নয় যেহেতু অন্যান্য ব্যবহারকারীও এগুলি দেখতে পান।",
index 937712b..a8bdd41 100644 (file)
        "file-thumbnail-no": "Jméno souboru začíná na <strong>$1</strong>.\nMožná to je obrázek ve zmenšené velikosti ''(náhled)''.\nNačtěte soubor v plném rozlišením, pokud je k dispozici, nebo změňte jméno souboru.",
        "fileexists-forbidden": "Soubor s tímto názvem již existuje a není dovoleno ho přepsat.\nPokud chcete přesto soubor načíst, vraťte se a zvolte jiný název.\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Soubor s tímto názvem již existuje ve sdíleném úložišti. Pokud přesto chcete váš soubor načíst, vraťte se a zvolte jiný název. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Načítaný soubor je přesným duplikátem aktuální revize souboru <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "Načítaný soubor je přesným duplikátem {{PLURAL:$2|starší revize|starších revizí}} souboru <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Tento soubor je duplikát {{PLURAL:$1|následujícího souboru|následujících souborů}}:",
        "file-deleted-duplicate": "Identický soubor k tomuto ([[:$1]]) byl již dříve smazán. Před tím, než soubor znovu nahrajete, byste měli zkontrolovat záznamy o předchozím smazání.",
        "file-deleted-duplicate-notitle": "Identický soubor k tomuto byl již dříve smazán a název byl utajen.\nPřed tím, než soubor znovu nahrajete, byste měli požádat někoho, kdo může prohlížet utajené soubory, aby situaci zkontroloval.",
        "changecontentmodel-nodirectediting": "Model obsahu $1 nepodporuje přímou editaci",
        "changecontentmodel-emptymodels-title": "Nejsou k dispozici žádné modely obsahu",
        "changecontentmodel-emptymodels-text": "Obsah stránky [[:$1]] nelze zkonvertovat na žádný typ.",
-       "log-name-contentmodel": "Kniha změny modelů obsahu",
+       "log-name-contentmodel": "Kniha změn modelů obsahu",
        "log-description-contentmodel": "Události týkající se modelů obsahu stránek",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|založil|založila}} stránku $3 za použití nestandardního modelu obsahu „$5“",
        "logentry-contentmodel-change": "$1 {{GENDER:$2|změnil|změnila}} model obsahu stránky $3 z „$4“ na „$5“",
index 4f2c815..e42a143 100644 (file)
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
        "articlepage": "Pera zerreki bıvin",
-       "talk": "Werênayış",
+       "talk": "Vaten",
        "views": "Asayışi",
        "toolbox": "Haceti",
        "userpage": "Pela karberi bıvêne",
        "action-managechangetags": "Vıraz u etiketa aktiv (me) ke",
        "action-applychangetags": "Vurnayışana piya etiket kerdışi zi dezge fi",
        "action-purge": "Ane perer newe ke",
-       "nchanges": "$1 {{PLURAL:$1|fın vurneya|fıni vurneya}}",
+       "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}",
-       "enhancedrc-history": "Veror",
+       "enhancedrc-history": "tarix",
        "recentchanges": "Vurriyayışê peyêni",
        "recentchanges-legend": "Tercihê vurnayışanê peyênan",
        "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.",
        "intentionallyblankpage": "Ena pel bi zanayişî weng mendo.",
        "external_image_whitelist": "  #no satır zey xo verde/raverde<pre>\n#parçeyê ifadeya rêzbiyayeyani (têna zerreyê ıney de // ) u çıtayo/çiyo zi mende cêr de têare kerê.\n#ney URL ya (hotlink) resmê teberi de hemcıta benî.\n#Ê yê ke hemcıt (eşleşmek-hemçift) biyê zey resımi asenî, eqsê hal de zi zey gıreyê resmi aseno.\nsatır ê ke pê ney # # destpêkenê zey mışore/mıjore muamele vineno.\n#herfa gırd û qıci ferq nêkeno\n\n#parçeyê ifadeya rêzbiyayeyani bıerzê serê ney satıri. no satır zey xo verde/raverde </pre>",
        "tags": "Ravêrde vurnayışanê etiketa",
-       "tag-filter": "Avrëcë [[Special:Tags|etiketi]]:",
+       "tag-filter": "Parzûnê [[Special:Tags|etiketi]]:",
        "tag-filter-submit": "Avrêc",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etiket|Etiketi}}]]: $2)",
        "tags-title": "Etiketan",
index 109004c..5fca9ea 100644 (file)
@@ -63,7 +63,7 @@
        "editfont-serif": "सेरिफ फन्ट",
        "sunday": "आइतबार",
        "monday": "सौउबार",
-       "tuesday": "माà¤\82à¤\97लबार",
+       "tuesday": "माà¤\99लबार",
        "wednesday": "बुधबार",
        "thursday": "बिपैबार",
        "friday": "शुकबार",
        "october-gen": "अक्टोबर",
        "november-gen": "नोभेम्बर",
        "december-gen": "डिसेम्बर",
-       "jan": "जनवरी",
+       "jan": "जन",
        "feb": "फेब्रुअरी",
        "mar": "मार्च",
-       "apr": "अप्रि",
+       "apr": "अप्रि",
        "may": "मे",
        "jun": "जुन",
-       "jul": "जुलाई",
-       "aug": "अगस्ट",
-       "sep": "सेप्टेम्बर",
-       "oct": "अक्टोबर",
-       "nov": "नोभेम्बर",
-       "dec": "डिसेम्बर",
+       "jul": "जुल",
+       "aug": "अग",
+       "sep": "सेप्ट",
+       "oct": "अक्ट",
+       "nov": "नोभ",
+       "dec": "डिस",
        "january-date": "जनवरी $1",
        "february-date": "फेब्रुअरी $1",
        "march-date": "मार्च $1",
        "december-date": "डिसेम्बर $1",
        "period-am": "रात १२ बज्या बठे छाकला सम्म",
        "period-pm": "छाकला बठे रात १२ बज्या सम्म",
-       "pagecategories": "{{PLURAL:$1|शà¥\8dरà¥\87णà¥\80|शà¥\8dरà¥\87णà¥\80हरà¥\82}}",
+       "pagecategories": "{{PLURAL:$1|शà¥\8dरà¥\87णà¥\80|शà¥\8dरà¥\87णà¥\80न}}",
        "category_header": "\"$1\" श्रेणीमी भया लेखहरू",
        "subcategories": "उपश्रेणीहरू",
        "category-media-header": "\"$1\" श्रेणीमी भया लेखहरू",
        "mytalk": "मेरी कुरडी",
        "anontalk": "कुरडी",
        "navigation": "खोज",
-       "and": "&#32;र",
+       "and": "&#32;र",
        "qbfind": "तम जाण",
        "qbbrowse": "ब्राउज गर्न्या",
        "qbedit": "सम्पादन",
        "faqpage": "Project:भौत सोधिएका प्रश्नहरु",
        "actions": "कार्यहरू",
        "namespaces": "नेमस्पेस",
-       "variants": "बहà¥\81रà¥\81पहरà¥\82",
+       "variants": "बहà¥\81रà¥\81पà¤\85न",
        "navigation-heading": "नेविगेशन मेनू",
        "errorpagetitle": "त्रुटी",
        "returnto": "$1 मी फर्क।",
-       "tagline": "{{SITENAME}}बाट",
-       "help": "सहायता",
-       "search": "खोज",
+       "tagline": "{{SITENAME}} बठेइ",
+       "help": "मदà¥\8dदत",
+       "search": "खोज",
        "search-ignored-headings": " #<!-- leave this line exactly as it is --> <pre>\n# Headings that will be ignored by search.\n# Changes to this take effect as soon as the page with the heading is indexed.\n# You can force page reindexing by doing a null edit.\n# The syntax is as follows:\n#   * Everything from a \"#\" character to the end of the line is a comment.\n#   * Every non-blank line is the exact title to ignore, case and everything.\nReferences\nExternal links\nSee also\n #</pre> <!-- leave this line exactly as it is -->",
-       "searchbutton": "खोज",
+       "searchbutton": "खोज:",
        "go": "जाने",
        "searcharticle": "जाओ",
        "history": "पाना इतिहास",
        "history_short": "पानाको इतिहास",
        "updatedmarker": "मेरो अन्तिम घुमाई पछि अद्यतन गरियाको",
-       "printableversion": "à¤\9bापà¥\8dनसà¤\95िनà¥\87 संस्करण",
+       "printableversion": "à¤\9bापà¥\8dद à¤®à¤¿à¤²à¥\8dलà¥\8dया संस्करण",
        "permalink": "स्थायी लिङ्क",
        "print": "छाप",
        "view": "अवलोकन गर",
        "unprotectthispage": "यै पानाको सुरक्षा परिवर्तन गर",
        "newpage": "नयाँ पाना",
        "talkpage": "यै पानाका बारेमी छलफल गर",
-       "talkpagelinktext": "à¤\95à¥\81रडà¥\80",
+       "talkpagelinktext": "à¤\95à¥\81रणि",
        "specialpage": "खास पानो",
-       "personaltools": "वà¥\8dयà¤\95à¥\8dतिà¤\97त à¤\94à¤\9cारहरà¥\82",
+       "personaltools": "वà¥\8dयà¤\95à¥\8dतिà¤\97त à¤\94à¤\9cारà¤\85न",
        "articlepage": "कन्टेन्ट पानो हेर",
-       "talk": "à¤\95à¥\81रडà¥\80 à¤\95ानी",
-       "views": "अवलोकन गर",
-       "toolbox": "à¤\94à¤\9cारहरà¥\82",
+       "talk": "à¤\95à¥\81रणिà¤\95ाà¤\86नी",
+       "views": "अवलोकन गर",
+       "toolbox": "à¤\94à¤\9cारà¤\85न",
        "userpage": "प्रयोगकर्ता पाना हेर्न्या",
        "projectpage": "प्रोजेक्ट पानो हेर्न्या",
        "imagepage": "चित्र पानो हेर",
        "viewhelppage": "सहायता पानो हेर्ने",
        "categorypage": "श्रेणी पानो हेर",
        "viewtalkpage": "छलफल हेर",
-       "otherlanguages": "à¤\85नà¥\8dय à¤­à¤¾à¤·à¤¾मी",
+       "otherlanguages": "à¤\94र à¤­à¤·à¤¾à¤\85नमी",
        "redirectedfrom": "($1 बाट पठाइयाको)",
        "redirectpagesub": "अनुप्रेषित पानो",
        "redirectto": "पठाएको पाना:",
-       "lastmodifiedat": "यà¥\88 à¤ªà¤¾à¤¨à¤¾à¤²à¤¾à¤\88 à¤\86नà¥\8dतिम à¤ªà¤\9fà¤\95 $2, $1 à¤®à¥\80 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रिया थ्यो।",
+       "lastmodifiedat": "यà¥\88 à¤ªà¤¨à¥\8dनालाà¤\88 à¤\9bाडà¥\8dडà¥\80बाऱ $2 à¤¬à¤\9cà¥\87, $1 à¤®à¥\80 à¤¹à¥\87रफà¥\87र à¤\97रियाऽ थ्यो।",
        "viewcount": "यो पाना हेरियाको थियो {{PLURAL:$1|एकपटक|$1 पटक}}",
        "protectedpage": "सुरक्षित गर्याका पानाहरू",
-       "jumpto": "यà¥\88मà¥\80 à¤\9cाà¤\93:",
-       "jumptonavigation": "भ्रमण गर",
-       "jumptosearch": "खोज",
+       "jumpto": "यà¥\88मà¥\80 à¤«à¤\9fà¥\8dà¤\9fाà¤\95:",
+       "jumptonavigation": "भ्रमण गर",
+       "jumptosearch": "खोज",
        "view-pool-error": "माफ गर्या , अहिल सर्भरहरूमी कामको भार भौत रह्या छ।\nभौत भौत प्रयोगकर्ताहरू यै पाना हेद्या प्रयास गरी रह्या छन्।\nकृपया यो पाना पुन: हेर्नु अगाडि थोक्कै पख ।\n\n$1",
        "generic-pool-error": "माफ गर्या , अहिल सर्भरहरूमी कामको भार भौत रह्या छ।\nभौत भौत प्रयोगकर्ताहरू यै पाना हेद्या प्रयास गरी रह्या छन् ।\nकृपया यो पाना पुन: हेर्नु अगाडि थोक्कै पख ।",
        "pool-timeout": "समय सकियो बन्द गर्ने प्रतीक्षामी",
        "pool-errorunknown": "अज्ञात गल्ती",
        "pool-servererror": "पुल काउन्टर सेवा उपलब्ध नाइथिन् ($1)।",
        "poolcounter-usage-error": "प्रयोग गल्ती:$1",
-       "aboutsite": "{{SITENAME}}à¤\95à¥\8b बारेमी",
+       "aboutsite": "{{SITENAME}}à¤\86 बारेमी",
        "aboutpage": "Project:बारेमी",
        "copyright": "सामाग्री $1 अनुसार उपलब्ध छ, खुलाइएको अवस्था बाहेकका हकमी ।",
        "copyrightpage": "{{ns:project}}:प्रतिलिपी अधिकारहरू",
        "currentevents": "आजभोलका घटनाहरू",
        "currentevents-url": "Project:आजभोलका घटनाहरू",
-       "disclaimers": "à¤\85सà¥\8dविà¤\95ारà¥\8bà¤\95à¥\8dतिहरà¥\82",
+       "disclaimers": "à¤\85सà¥\8dविà¤\95ारà¥\8bà¤\95à¥\8dतà¥\80न",
        "disclaimerpage": "Project:सामान्य अस्वीकारोक्ति",
        "edithelp": "सम्पादन सहायता",
        "helppage-top-gethelp": "सहायता",
-       "mainpage": "मà¥\81à¤\96à¥\8dय à¤ªà¤¾à¤¨à¥\8b",
-       "mainpage-description": "मà¥\81à¤\96à¥\8dय à¤ªà¤¾à¤¨à¥\8b",
+       "mainpage": "मà¥\81à¤\96à¥\8dय à¤ªà¤¨à¥\8dना",
+       "mainpage-description": "मà¥\81à¤\96à¥\8dय à¤ªà¤¨à¥\8dना",
        "policy-url": "Project:निति",
        "portal": "सामाजिक पोर्टल",
        "portal-url": "Project:सामाजिक पोर्टल",
        "versionrequired": "MediaWiki संस्करण $1 चाईन्या",
        "versionrequiredtext": "ये पाना प्रयोग गर्नका लागि MediaWiki $1 संस्करण चाहिन्छ ।\nहेर  [[Special:Version|version page]]",
        "ok": "भयो",
-       "retrievedfrom": " \"$1\" बठे निकालिया",
+       "retrievedfrom": " \"$1\" बठे निकालिया",
        "youhavenewmessages": "{{PLURAL:$3|तम सित छन}} $1 ($2)।",
        "youhavenewmessagesfromusers": "तमखी लेखा {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्तान}}($2)बठे$1",
        "youhavenewmessagesmanyusers": "तमलाई धेरै प्रयोगकर्ताहरू($2) बठे $1 छ ।",
        "viewsourceold": "स्रोत हेर",
        "editlink": "सम्पादन",
        "viewsourcelink": "स्रोत हेर",
-       "editsectionhint": "खण्ड: $1 सम्पादन गर",
+       "editsectionhint": "खण्ड: $1 सम्पादन गर",
        "toc": "विषयसूची",
        "showtoc": "धेकाउन्या",
        "hidetoc": "लुकाउन्या",
        "feed-invalid": "अमान्य फिड प्रकार ग्राह्याता ।",
        "feed-unavailable": "सिन्डीकेसन फिडहरु उपलब्ध नाइथिन्",
        "site-rss-feed": "$1 आरएसएस फिड",
-       "site-atom-feed": "$1 à¤\8fà¤\9fम à¤«à¤¿ड",
+       "site-atom-feed": "$1 à¤\8fà¤\9fम à¤«à¥\80ड",
        "page-rss-feed": "\"$1\" आरएसएस फिड",
        "page-atom-feed": "\"$1\" एटम फिड",
-       "red-link-title": "$1 (पाना उपलब्ध नाइँथिन)",
+       "red-link-title": "$1 (पनà¥\8dना उपलब्ध नाइँथिन)",
        "sort-descending": "अवरोहण क्रममी मिलाउन्या",
        "sort-ascending": "आरोहण क्रममी मिलाउन्या",
-       "nstab-main": "लà¥\87à¤\96",
+       "nstab-main": "पनà¥\8dना",
        "nstab-user": "प्रयोगकर्ता पानो",
        "nstab-media": "माध्यम पाना",
        "nstab-special": "खास पानो",
        "nstab-template": "ढाँचा",
        "nstab-help": "सहायता पानो",
        "nstab-category": "श्रेणी",
-       "mainpage-nstab": "मà¥\81à¤\96à¥\8dय à¤ªà¤¾à¤¨à¥\8b",
+       "mainpage-nstab": "मà¥\81à¤\96à¥\8dय à¤ªà¤¨à¥\8dना",
        "nosuchaction": "यसो काम हैन",
        "nosuchactiontext": "URL ले खुलाएको काम मान्य छैन ।\nतमीले URL गलत टाइपगरेका हौ , वा गलत लिंकक पछाडी लागेका हुनसक्देहौ ।\nयो {{SITENAME}}ले सफ्टवेयरमी भयाको गल्ति देखायाको लै हुनसक्छ ।",
        "nosuchspecialpage": "तसो विशेष पानो छैन",
        "pt-login": "प्रवेश (लग ईन)",
        "pt-login-button": "प्रवेश",
        "pt-login-continue-button": "प्रवेश जारी राख",
-       "pt-createaccount": "नयाँ खाता खोल",
+       "pt-createaccount": "नयाँ खाता खोल:",
        "pt-userlogout": "बाहिर निस्कन्या (लग आउट)",
        "php-mail-error-unknown": "PHP मेल() क्रियामा अज्ञात गल्ती",
        "user-mail-no-addy": "इमेल ठेगाना बिनाई इमेल पठाउन खोजिया थ्यो।",
        "newpageletter": "नौ",
        "boteditletter": "बो",
        "rc_categories": "श्रेणीहरूमी सीमित (\"|\" ले छुट्याओ)",
-       "rc-change-size-new": "$1 {{PLURAL:$1|बाà¤\87à¤\9f|बाà¤\87à¤\9fस}}फà¥\87रबदलपाछा",
+       "rc-change-size-new": "$1 {{PLURAL:$1|बाà¤\87à¤\9f|बाà¤\87à¤\9fà¥\8dस}}फà¥\87रबदल पाछा",
        "recentchangeslinked": "सम्बन्धित फेरबदल",
        "recentchangeslinked-toolbox": "सम्बन्धित फेरबदल",
        "recentchangeslinked-title": "\"$1\" सित सम्बन्धित परिवर्तन",
        "recentchangeslinked-summary": "यो सूची निर्दिष्ट पाना (वा निर्दिष्ट श्रेणी)सित जोडियाका अल्लै परिवर्तन भयाका पानाको  हो। [[Special:Watchlist|तमरो ध्यानसूची]]का पानाहरू <strong>गाढा अक्षरमी</strong> छन्।",
        "recentchangeslinked-page": "पाना नाम:",
        "recentchangeslinked-to": "यैको सट्टा यो पानासित जोडियाका पानानको परिवर्तन धेकाउन्या",
-       "upload": "à¤\9aितà¥\8dर à¤\85पलà¥\8bड à¤\97र",
+       "upload": "फाà¤\87ल à¤\85पलà¥\8bड à¤\97रऽ",
        "uploadbtn": "फाइल अपलोड गर्न्या",
        "upload-recreate-warning": "'''चेतावनी: त्यस नाममी रह्याका फाइलहरू सारियाको या हटायाको छ।'''\n\nयै पानाको सारियाको र हटायाको लग तमरो सहजताको लागि दियाको छ।",
        "filedesc": "सारांश:",
        "large-file": "यो सिफारिस गर्याछकि फाइलहरूको आकार $1 भन्दा ठूला हुनु हुँदैन;\nयै फाइलको आकार $2 छ ।",
        "emptyfile": "तमीले अपलोड गर्याको फाइल रित्तो छ ।\nयो फाइल नाम गलत राख्याका कारणले भयाको हुनसकन्छ\nयो फाइल साँच्चै अपलोड गद्दे कुरडीमी निश्चित होइजाओ ।",
        "fileexists": "यै नामको फाइल पैल्ली नैं छ, यदि तम परिवर्तन गद्या कुरडीमू सुनिश्चित छैनौ भण्या कृपया <strong>[[:$1]]</strong> जाँच गर।\n[[$1|thumb]]",
+       "fileexists-no-change": "अपलोड <strong>[[:$1]]</strong>का अच्यालआ संस्करणो ठ्याक्कै नकल हो।",
+       "fileexists-duplicate-version": "अपलोड <strong>[[:$1]]</strong> को {{PLURAL:$2|पुरानु संस्करण|पुरानु संस्करणअन}}ओ नकल हो।",
        "filewasdeleted": "यै नामको एक फाइल पहिली पनि अपलोड गरिबर पछि हटाई सकियाको छ।\nपुनः अपलोड गद्दु पूर्व तम $1 लाई निक्करी जाँच गर ।",
        "upload-dialog-title": "चित्र अपलोड गर",
        "upload-dialog-button-cancel": "रद्द",
        "filedelete-intro-old": "तमी <strong>[[Media:$1|$1]]</strong> को संस्करणलाई [$4 $3, $2] हुन्या गरि मेट्ट लाग्याछौ ।",
        "filedelete-maintenance": "रखरखाव चलिरह्याको हुनाले अस्थायी रुपमी फाइलहरू मेट्ट्या र मेट्याकोलाई पुनर्बहाली गर्न निष्क्रिय गरियाकोछ।",
        "mimesearch-summary": "MIME-प्रकार अनुसार फाइलहरू खोज्न यै पानाको प्रयोग गद्द सकिन्याछ ।\nइनपुट: फाइलको प्रकार/उपप्रकार, उदा. <code>image/jpeg</code>।",
-       "randompage": "à¤\95à¥\8bà¤\87 à¤\8fà¤\95 à¤²à¥\87à¤\96",
+       "randompage": "à¤\95à¥\8dरमरहित à¤ªà¤¨à¥\8dना",
        "statistics-header-pages": "पानानको तथ्याङ्क",
        "statistics-header-edits": "सम्पादनहरूको तथ्याङ्क",
        "statistics-files": "अपलोड गर्याका फाइलहरू",
        "ipblocklist": "ब्लक गर्याका प्रयोगकर्ताहरू",
        "ipblocklist-legend": "ब्लक गर्याका प्रयोगकर्ताहरू खोज",
        "blocklink": "रोक्न्या",
-       "contribslink": "यà¥\8bà¤\97दानहरà¥\82",
+       "contribslink": "यà¥\8bà¤\97दानà¤\85न",
        "block-log-flags-anononly": "नाम नभयाका प्रयोकर्ताहरू मात्र",
        "proxyblockreason": "तमरो IP ठेगानामी रोक लगायाको छ किनकी यो खुला प्रोक्सी हो ।\nकृपया तमरो इन्टरनेट सेवा प्रदायक या प्राविधिक सहायतासँग सम्पर्क गरीबर यै सुरक्षा समस्याका बारेमी जानकारी गराओ ।",
        "sorbsreason": "तमरो IP ठेगाना खुल्ला प्रोक्सीको रुपमी  DNSBL मा सूचीकरण गरिएको छ यैलाई{{SITENAME}}ले प्रयोगमी ल्यायाको छ।",
        "tooltip-pt-preferences": "{{GENDER:|तमरी}} अभिरुचि",
        "tooltip-pt-watchlist": "पृष्ठहरूको सूची जैका फेरबदलहरुलाई तमले पहरा गरिराखेका छौ ।",
        "tooltip-pt-mycontris": "{{GENDER:|तमरा}} योगदानअनऐ सूची",
-       "tooltip-pt-login": "तमलाई प्रवेशगद्द सुझाव दिइन्छ ; याद अर यो जरुरी आथिन भण्या ।",
+       "tooltip-pt-login": "तमलाई प्रवेशगद्द सुझाव दिइन्छ ; याद अर यो जरुरी आथिन भण्या ।",
        "tooltip-pt-logout": "बाहिर निस्कन्या (लग आउट)",
        "tooltip-pt-createaccount": "तमलाई खाता बनौन लै लग इन अद्द हम हौसला अद्दाउ; काइकि, यो अनिवार्य नाइथी भण्या ।",
-       "tooltip-ca-talk": "सामाà¤\97à¥\8dरà¥\80 à¤ªà¥\83षà¥\8dठबारà¥\87मà¥\80 à¤\9bलफल",
-       "tooltip-ca-edit": "ये पाना सम्पादन गर",
+       "tooltip-ca-talk": "सामाà¤\97à¥\8dरà¥\80 à¤ªà¥\83षà¥\8dठबारà¥\87मà¥\80 à¤\95à¥\81रणिà¤\95ाà¤\86नà¥\80",
+       "tooltip-ca-edit": "येइ पन्ना सम्पादन गरऽ",
        "tooltip-ca-addsection": "नयाँ खण्ड सुरु अरिदिय",
        "tooltip-ca-viewsource": "यो पानो सुरक्षित अरियाको छ। यैको श्रोत हेद्द सकन्छौ ।",
-       "tooltip-ca-history": "यà¥\88 à¤ªà¥\83षà¥\8dठà¤\95ा à¤ªà¥\88लà¥\8dलिà¤\95ा à¤ªà¥\81नरावलà¥\8bà¤\95नहरà¥\81",
+       "tooltip-ca-history": "यà¥\88 à¤ªà¤¨à¥\8dनाऽ à¤ªà¥\88लà¥\8dलिà¤\95ा à¤ªà¥\81नरावलà¥\8bà¤\95नà¤\85न",
        "tooltip-ca-undelete": "मेट्याको भया पनि यै पानाको सम्पादनहरू पुन:प्राप्त गर",
        "tooltip-ca-move": "यो पानालाई अर्खिठौर सार",
        "tooltip-ca-watch": "यै पानालाई तमरा ध्यानसूचीमि थपिदिय",
-       "tooltip-search": "{{SITENAME}}मी खोज",
-       "tooltip-search-go": "यदि à¤¯à¥\8b à¤¨à¤¾à¤®à¤\95à¥\8b à¤ªà¥\83षà¥\8dठ à¤°à¤¯à¤¾à¤\95à¥\8b à¤\9b à¤­à¤£à¥\8dया à¤¤à¥\88मà¥\80 à¤\9cानà¥\8dया ।",
-       "tooltip-search-fulltext": "यà¥\88 à¤ªà¤¾à¤ à¤\95ा à¤²à¤¾à¤\97ि à¤ªà¤¾à¤¨à¤¾मी खोज",
-       "tooltip-p-logo": "à¤\96ास à¤ªà¤¾à¤¨à¥\8b",
+       "tooltip-search": "{{SITENAME}}मी खोज",
+       "tooltip-search-go": "यदà¥\80 à¤ à¥\8dयाà¤\95à¥\8dà¤\95à¥\88 à¤¯à¥\87à¤\87 à¤¨à¤¾à¤\89à¤\81 à¤­à¤¯à¤¾: à¤ªà¤¨à¥\8dना à¤°à¥\88à¤\9b à¤­à¤\81णà¥\8dया à¤¤à¥\88 à¤®à¥\80 à¤\9cा:।",
+       "tooltip-search-fulltext": "यà¥\88 à¤ªà¤¾à¤ à¤\95ा à¤²à¤¾à¤\97ि à¤ªà¤¨à¥\8dनाà¤\85नमी खोज",
+       "tooltip-p-logo": "मà¥\81à¤\96à¥\8dय à¤ªà¤¨à¥\8dनामà¥\80 à¤¹à¥\87रऽ",
        "tooltip-n-mainpage": "खास पानामी झान्या",
-       "tooltip-n-mainpage-description": "à¤\96ास à¤ªà¤¾à¤¨à¤¾à¤®à¥\80 à¤\9dा",
+       "tooltip-n-mainpage-description": "à¤\96ास à¤ªà¤¨à¥\8dनामà¥\80 à¤\9dाऽ",
        "tooltip-n-portal": "आयोजनाका बारेमी , तम कि अद्द सकन्छौ , समान काखाइ  भेटौन्या",
        "tooltip-n-currentevents": "हालैका घटनाको बारेमी पृष्ठभूमि जानकारी पत्ता लागाइदिय",
-       "tooltip-n-recentchanges": "विà¤\95िमा à¤\85रियाà¤\95ा à¤¹à¤¾à¤²à¥\88à¤\95ा à¤­à¤¯à¤¾ à¤«à¥\87रबदलà¤\95ा शुचि ।",
-       "tooltip-n-randompage": "à¤\9cà¥\8b à¤\95à¥\8bà¤\87 à¤ªà¤¾à¤¨à¥\8b à¤\96à¥\8bलà¥\8dया",
+       "tooltip-n-recentchanges": "विà¤\95िमà¥\80 à¤¹à¤¾à¤²à¥\88 à¤\85रियाà¤\95ा à¤«à¥\87रबदलà¥\88 शुचि ।",
+       "tooltip-n-randompage": "à¤\95à¥\8dरमरहित à¤ªà¤¨à¥\8dना à¤\96à¥\8bलऽ",
        "tooltip-n-help": "खोज्जु पड्या ठौर ।",
-       "tooltip-t-whatlinkshere": "यà¥\8b à¤¸à¤¿à¤¤ à¤\9cà¥\8bडियाà¤\95ा à¤¸à¤¬à¥\8dबà¥\88 à¤µà¤¿à¤\95ि à¤ªà¤¾à¤¨à¤¾à¤¨à¤\95à¥\8b à¤¸à¥\82à¤\9aà¥\80",
+       "tooltip-t-whatlinkshere": "सपà¥\8dपà¥\88 à¤µà¤¿à¤\95ि à¤ªà¤¨à¥\8dनाà¤\85नà¥\88 à¤¶à¥\81à¤\9aि à¤\9cà¥\8b à¤¯à¤¾à¤\81à¤\96ाà¤\87 à¤\9cà¥\8bणà¥\80à¤\9cान",
        "tooltip-t-recentchangeslinked": "यै पानामी जोडियाका पानामी अहिलको परिवर्तन",
        "tooltip-feed-atom": "यै पानाकी लेखा एक एटम फिड",
        "tooltip-t-contributions": "{{GENDER:$1|यिन प्रयोगकर्ता}}का योगदानहरूको सूची हेरपुई",
-       "tooltip-t-upload": "à¤\9aितà¥\8dर à¤\85पà¥\8dलà¥\8bड à¤\85र",
-       "tooltip-t-specialpages": "सब्बै खास खास पानानको शुचि ।",
-       "tooltip-t-print": "यà¥\8b à¤ªà¤¾à¤¨à¤¾à¤\95à¥\8b à¤\9bापिन्या संस्करण",
+       "tooltip-t-upload": "फाà¤\87ल à¤\85पà¥\8dलà¥\8bड à¤\85रऽ",
+       "tooltip-t-specialpages": "सब्बै खास-खास पन्नाअनै सूची",
+       "tooltip-t-print": "यà¥\87à¤\87 à¤ªà¤¨à¥\8dनाऽ à¤\9bापà¥\8dद à¤®à¤¿à¤²à¥\8dल्या संस्करण",
        "tooltip-t-permalink": "पृष्ठको यो पुनरावलोकनकि लेखा स्थाई लिङ्क",
        "tooltip-ca-nstab-main": "सामाग्री पानो हेरिदिय",
        "tooltip-ca-nstab-user": "प्रयोगकर्ता पानो हेरिदिय",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|प्रयोगकर्ता|प्रयोगकर्ताहरू}} $1",
        "anonusers": "{{SITENAME}} का नाम नभयाका {{PLURAL:$2| प्रयोगकर्ता|प्रयोगकर्ताहरू}} $1",
        "simpleantispam-label": "ऐन्टी-स्प्याम जाँच।\nयैलाई <strong>नाइँ</strong> भद्य्या!",
-       "pageinfo-toolboxlink": "यà¥\88 à¤ªà¤¾à¤¨à¤¾à¤\95à¥\8b à¤\9cाणकारी",
+       "pageinfo-toolboxlink": "पनà¥\8dनाà¤\87 à¤\9cानकारी",
        "rcpatroldisabled": "अहिलका परिवर्तनहरू गस्ती निष्क्रिय पार्याको छ ।",
        "rcpatroldisabledtext": "अहिलका परिवर्तनहरू गस्ती गुण अहिलको लागि निष्कृय पारियाको छ ।",
        "markedaspatrollederror-noautopatrol": "तमी आफ्नै सम्पादनलाई गस्ती गरियाको भनि चिनो लगाउन नाइसक्दा ।",
        "watchlistedit-clear-done": "तमरो ध्यान सूची खाली गरीयाको छ।",
        "watchlisttools-view": "आधारित फेरबदलीहरू हेर",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|कुरडी]])",
-       "specialpages": "à¤\96ास à¤ªà¤¾à¤¨à¥\8b",
+       "specialpages": "à¤\96ास à¤ªà¤¨à¥\8dनाà¤\85न",
        "specialpages-note": "* साधारण खास पानाहरू।\n* <span class=\"mw-specialpagerestricted\">निषेधित खास पानाहरू।</span>",
        "specialpages-group-changes": "अल्लैका परिवर्तन लगहरू",
        "tags": "मान्य परिवर्तन ट्यागहरू",
        "logentry-newusers-create": "प्रयोगकर्ता खाता $1 {{GENDER:$2|खोलियो}}",
        "logentry-upload-upload": "$1 ले $3 {{GENDER:$2|अपलोड अरेका छन्}}",
        "feedback-bugornote": "यदि तमी कुनै प्राविधिक समस्यालाई विस्तारले सम्झाउन तयार छौ भण्या कृपया [$1 बग राख]।\nयदि हैन, भण्या तमी तल दियाको सरल फारमको प्रयोग गद्दसक्द्याहौ । तमरो टिप्पणी, तमरो प्रयोगकर्ता नाम र तमरो ब्राउजरको नाम सहित \"[$3 $2]\" पानामी जोडिन्याछ ।",
-       "searchsuggest-search": "खोज",
+       "searchsuggest-search": "खोज:",
        "api-error-duplicate": "यै साइटमी पहिलीबठे यस्तै सामग्री {{PLURAL:$1|भयाको अर्को फाइल छ|भयाका  केहि अरु फाइलहरू छन्}} ।",
        "api-error-duplicate-archive": "यै साइटमी पहिलेबाट यस्तै सामग्री {{PLURAL:$1|भयाको अर्को फाइल थियो|भयाका केहि अरु फाइलहरू थिए}} ।\nतर {{PLURAL:$1|यो मेट्याको थियो|यी मेटायाका थिए}} ।",
        "expand_templates_preview_fail_html": "<em>किनकि {{SITENAME}} सिधै एचटिएमयल सक्षम छ र तमीले लग इन गर्या छैनौ, पूर्वावलोकन लुकाइयाको छ ताकि सम्भावित जाभास्क्रिप्ट आक्रमणलाई रोक्द सकियोस् ।</em>\n\n<strong>यदि यो मान्य पूर्ववावलोकन प्रयास हो भण्या पुन प्रयास गर ।</strong>\nयदि यसले कार्य पूर्ण भएन भण्या [[Special:UserLogout|लग आउट गरिबर]] फेरी लग इन गर्या ।",
index bf093ed..d1d84b0 100644 (file)
        "newarticle": "(Nōv)",
        "newarticletext": "Al colegamèint apèina fât al cumbîna cun 'na pàgina ch' an n'é mìa incòra stêda fâta. S'ét vō fêr la pàgina adès, l'é asê cumincêr a scréver al tèst int la caşèla ché sòt (per vedèr infurmasiòun pió precîşi guêrda la [$1 pàgina 'd ajót]). Se al colegamèint  l'é stê avêrt per erōr, l'é asê clichêr al pulsânt \"Indrē\" dal tó navigadōr.",
        "anontalkpagetext": "----\n<em>Còsta l'è la pàgina 'd discusiòun ed 'n utèint sèinsa nòm, ch' an n' à mìa incòra fât 'n' utèinsa o in tót al manēri an n'è mìa drē druvêrla.</em> Per arcgnòsrel l'è dòunca necesâri druvê al nóme dal só indirés IP. J indirés IP a pōlen èser spartî cun êter utèint. Se t'é un utèint sèinsa nòm e 't pèins che i cumèint in cla pàgina ché an riguêrden mìa té, [[Special:CreateAccount|fa 'n' utèinsa nōva]] o [[Special:UserLogin|vîn dèinter cun còla ch' ét gh'ê bèle]] per schivşêr, in futûr,  'd èser cunfûş cun 'd j êter utèint sèinsa nòm.",
-       "noarticletext": "In cól mumèint ché la pàgina serchêda l'é vōda. L'é pusébil [[Special:Search/{{PAGENAME}}|serchêr sté tétol]] int al j êtri pàgini dal sît, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serchêr int i regéster coleghê] opór  [{{fullurl:{{FULLPAGENAME}}|action=edit}} mudifichêr la pàgina adèsa]</span>.",
+       "noarticletext": "In cól mumèint ché la pàgina serchêda l'é vōda.Ét pō\n[[Special:Search/{{PAGENAME}}|serchêr cól tétoi ché]] int al j êtri pàgini dal sît, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serchêr int i regéster coleghê] opór  [{{fullurl:{{FULLPAGENAME}}|action=edit}} e fêr cla pàgina ché]</span>.",
        "noarticletext-nopermission": "In cól mumèint ché la pàgina serchêda l'é vōda. L'é pusébil [[Special:Search/{{PAGENAME}}|serchêr sté tétol]] int al j êtri pàgini dal sît o<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} serchêr int i regéster coleghê] <span>, mó an 't gh'ê mìa al permès ed fêr cla pàgina ché.",
        "missing-revision": "La revişiòun #$1 'd la pagina \"{{FULLPAGENAME}}\" l' an gh'è mìa. Còst, ed sôlit, a sucēd mèint'r as va drē a 'n colegamèint a 'na pàgina scanşlêda, in 'na stòria, di lavōr fât, mìa arnuvêda. I particulêr a 's pōlen catêr int al [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} regéster dal scanşladûri].",
        "userpage-userdoesnotexist": "L'inscrisiòun \"<nowiki>$1</nowiki>\" la cumbîna mìa cun 'n utèint registrê. Ét sicûr ed vrèir fêr o mudifichêr cla pàgina ché.",
        "userpage-userdoesnotexist-view": "L'utèin \"$1\" an n'à mìa fât l'inscrisiòun.",
        "blocked-notice-logextract": "Cl'utèint ché adèsa l'é bluchê. \nPer infurmasiòun l'ûltem elemèint dal regéster di blôch l'é scrét ché sòta:",
-       "clearyourcache": "'''Nôta:''' dôpa vèir salvê a pré èser necesâri pulîr la memôria pruvişôria dal navigadôr per vèder i cambiamèint.\n*'''Firefox / Safari''': tgnîr cucê al tâst dal lètri grândi e clichêr insém a \"Ricarica\" opór cucêr i tâst ''Ctrl-F5'' o ''Ctrl-R'' (''⌘-R'' insém a un Mac)\n*'''Google Chrome''': cucêr i tâst ''Ctrl-Shift-R'' (''⌘-Shift-R'' insém a un Mac) \n*'''Internet Explorer''': tgnîr cucê al tâst ''Ctrl'' mènter es fà cléch insém a ''Refresh'', opór cucêr ''Ctrl-F5'' \n*'''Opera''': svudêr dal tót la memôria pruvişôria 'd la lésta ''Strumenti → Preferenze''",
+       "clearyourcache": "<strong>Nôta:</strong> dôpa vèir salvê a pré èser necesâri pulîr la memôria pruvişôria dal navigadôr per vèder i cambiamèint.\n*< strong >Firefox / Safari /<strong>: tgnîr cucê al tâst dal lètri grândi <em>Shift</em> e clichêr in sém a <em>Ricarica</em>, opór cucêr i tâst <em>Ctrl-F5</em> o <em>Ctrl-R</em> (<em>⌘-R</em> in sém a ‘n Mac)\n*<strong>Google Chrome:</strong>: cucêr i tâst <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> in sém a ‘n Mac) \n*<strong>Internet Explorer:</strong>: tgnîr cucê al tâst<em>Ctrl</em> e fêr cléch in sém a em>Aggiorna</em>, opór clichêr <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Và int al <em>Menu → Impostazioni</em> (<em>Opera → Preferenze</em> in séma 'n Mac)  e pó in <em>Privacy & sicurezza → Pulisci dati del browser → Immagini e file nella cache</em>.",
        "usercssyoucanpreview": "'''Cunséli:''' drōva al tâst 'Guêrda préma' per pruvêr al tó nōv CSS préma 'd salvêrel'''",
        "userjsyoucanpreview": "'''Cunséli:''' drōva al tâst 'Guêrda préma' per pruvêr al tó nōv  JavaScript préma 'd salvêrel'''",
        "usercsspreview": "'''Còsta l'é sōl 'na guardêda al tó CSS préma 'd salvêr al mudéfichi ch'în stêdi fâti.Ricôrdet che al mudéfichi în mìa incòra stêdi salvêdi!'''",
        "previewnote": "'''Ricôrdet che còsta l'é sōl 'na guardêda préma 'd salvêr.'''\nAl tō mudéfichi în MIA incòra stêdi salvêdi.",
        "continue-editing": "Và int la zôna 'd mudéfica",
        "previewconflict": "La vésta la cumbîna cun al tèst int la zôna 'd mudéfica tèst ché d'ed sōver e l'é cme la srà la pàgina s'ed decéd ed clichêr insém a \"Sêlva la pàgina\" in cól mumèint ché.",
-       "session_fail_preview": "'''An n'é mìa stê pusébil registrêr la mudéfica perchè a s' în pêrsi al j infurmasiòun relatîvi a la sesiòun. Tōrna a pruvêr. Se al prublēma al cunténva, a 's pōl pruvêr [[Special:UserLogout|ed coleghêres]] e fêr un ingrès nōv.'''",
+       "session_fail_preview": "A's în dispiêş. An n'é mìa stê pusébil registrêr la mudéfica perchè a 's în pêrsi al j infurmasiòun relatîvi a la sesiòun. Ét prés èser stê destachê. <strong>Contròla s' t'é incòra coleghê</strong>. Se al problēma 'l cunténva, a 's pōl pruvêr [[Special:UserLogout|ed coleghêres]] e fêr un ingrès nōv, contròla ânch se al tó navigadōr l' acèta i cookie da cól sît ché.",
        "session_fail_preview_html": "'''An n'é mìa stê pusébil registrêr la mudéfica perchè în andêdi persi al j infurmasiòun relatîvi a la sesiòun.'''\n\n''Pôst che in {{SITENAME}} a gh'é al permès ed druvêr l' HTML sèinsa lémit, an 's pōl mìa guardêr préma la pàgina mudifichêda; a 's trâta ed 'n'amzûra 'd sicurèsa cûntra j atâch JavaScript.''\n\n''' Se còst l'é un tentatîv legétim ed mudéfica, pruvêr incòra. Se al prublēma l'armâgn, a 's pōl pruvêr a [[Special:UserLogout|sarêr al colegamèint]] e fêr un nōv ingrès.'''",
        "token_suffix_mismatch": "'''La mudéfica an n'é mìa stêda salvêda perchè al ''client'' l'à fât vèder ed gestîr in môd e-sbaliê i carâter di pûn e dal virgûli int al ''token'' lighê a la mudéfica. Per schivşêr di pusébil erōr int al tèst ed la pàgina, è stê rifiutê tóta la mudéfica. Dla vôlti cla situasiòun ché la pōl sucēder quând a vînen druvê soquânt servési ''proxy'' sèinsa nòm via internèt che preşèinten di ''bug''.'''",
        "edit_form_incomplete": "'''Soquânti pêrt dal môdul ed mudéfica în mìa rivêdi al ''server''; controlêr che al mudéfichi sién intâti e turnêr a pruvêr'''",
index 020f058..c8969a7 100644 (file)
        "pageinfo-article-id": "Page ID",
        "pageinfo-language": "Page content language",
        "pageinfo-content-model": "Page content model",
+       "pageinfo-content-model-change": "change",
        "pageinfo-robot-policy": "Indexing by robots",
        "pageinfo-robot-index": "Allowed",
        "pageinfo-robot-noindex": "Disallowed",
index 3fdf9a7..dfab8c9 100644 (file)
        "filerevert-submit": "Revertir",
        "filerevert-success": "<strong>[[Media:$1|$1]]</strong> ha sido revertido a la [$4 versión del $2 a las $3].",
        "filerevert-badversion": "No existe versión local previa de este archivo con esa marca de tiempo.",
+       "filerevert-identical": "La versión actual del archivo ya es idéntica a la seleccionada.",
        "filedelete": "Borrar $1",
        "filedelete-legend": "Borrar archivo",
        "filedelete-intro": "Estás por borrar el archivo <strong>[[Media:$1|$1]]</strong> así como todo su historial.",
index 6695548..4d4e0ae 100644 (file)
        "file-thumbnail-no": "Le nom du fichier commence par <strong>$1</strong>.\nIl est possible qu'il s'agisse d'une version réduite <em>(vignette)</em>.\nSi vous disposez du fichier en haute résolution, importez-le, sinon veuillez modifier son nom.",
        "fileexists-forbidden": "Un fichier avec ce nom existe déjà et ne peut pas être écrasé.\nSi vous voulez toujours importer votre fichier, veuillez revenir en arrière et utiliser un autre nom. \n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Un fichier portant ce nom existe déjà dans le dépôt de fichiers partagé.\nSi vous voulez toujours importer votre fichier, veuillez revenir en arrière et utiliser un autre nom. \n[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Le fichier téléchargé est une copie exacte de la version actuelle de <strong>[[:$1]]</strong>",
+       "fileexists-duplicate-version": "Le fichier téléchargé est une copie exacte {{PLURAL:$2|d'une version précédente|de versions précédentes}} de <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Ce fichier est un doublon {{PLURAL:$1|du fichier suivant|des fichiers suivants}} :",
        "file-deleted-duplicate": "Un fichier identique à celui-ci ([[:$1]]) a déjà été supprimé. \nVous devriez vérifier le journal des suppressions de ce fichier avant de l'importer à nouveau.",
        "file-deleted-duplicate-notitle": "Un fichier identique à ce fichier a déjà été supprimé ainsi que le titre. \nVous devriez demander à quelqu'un la possibilité de vérifier le journal de ce fichier supprimé afin d'examiner la situation  avant de l'importer à nouveau.",
        "file-info-png-looped": "en boucle",
        "file-info-png-repeat": "joué $1 {{PLURAL:$1|fois}}",
        "file-info-png-frames": "$1 {{PLURAL:$1|image|images}}",
-       "file-no-thumb-animation": "'''Remarque : En raison de limitations techniques, les vignettes de ce fichier ne seront pas animées.'''",
-       "file-no-thumb-animation-gif": "'''Remarque : En raison de limitations techniques, les vignettes d'images GIF en haute résolution telles que celle-ci ne seront pas animées.'''",
+       "file-no-thumb-animation": "<strong>Remarque : En raison de limitations techniques, les vignettes de ce fichier ne seront pas animées.</strong>",
+       "file-no-thumb-animation-gif": "<strong>Remarque : En raison de limitations techniques, les vignettes d'images GIF en haute résolution telles que celle-ci ne seront pas animées.</strong>",
        "newimages": "Galerie des nouveaux fichiers",
-       "imagelisttext": "Voici une liste de '''$1''' fichier{{PLURAL:$1||s}} classée $2.",
+       "imagelisttext": "Voici une liste de <strong>$1</strong> {{PLURAL:$1|fichier classé|fichiers classés}} $2.",
        "newimages-summary": "Cette page spéciale affiche les derniers fichiers importés.",
-       "newimages-legend": "Nom du fichier",
+       "newimages-legend": "Filtre",
        "newimages-label": "Nom du fichier (ou une partie de celui-ci) :",
-       "newimages-showbots": "Afficher les imports par des robots",
+       "newimages-showbots": "Afficher les imports faits par des robots",
        "newimages-hidepatrolled": "Masquer les téléchargements patrouillés",
        "noimages": "Aucune image à afficher.",
        "ilsubmit": "Rechercher",
index 88d16af..20f83e5 100644 (file)
        "minoredit": "Սա չնչին խմբագրում է",
        "watchthis": "Հսկել այս էջը",
        "savearticle": "Հիշել էջը",
+       "savechanges": "Պահպանել փոփոխությունները",
        "publishpage": "Հիշել փոփոխությունները",
        "publishchanges": "Հիշել փոփոխությունները",
        "preview": "Նախադիտում",
index 3ba5e7d..5f451b9 100644 (file)
@@ -71,7 +71,8 @@
                        "Kana Higashikawa",
                        "Shield-9",
                        "Waiesu",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "組曲師"
                ]
        },
        "tog-underline": "リンクの下線:",
        "feedback-terms": "私のユーザーエージェント情報には、使用ブラウザやオペレーティングシステムのバージョンの情報が含まれており、その情報は私が提供するフィードバックとあわせて公開されることを理解しました。",
        "feedback-termsofuse": "利用規約に従い、フィードバックを提供することに同意します。",
        "feedback-thanks": "ありがとうございます。フィードバックを「[$2 $1]」のページに投稿しました。",
-       "feedback-thanks-title": "ã\81\82ã\82\8aã\81\8cã\81¨ã\81\86ã\81\94ã\81\96ã\81\84ã\81¾ã\81\99!",
+       "feedback-thanks-title": "ã\81\8aé¡\98ã\81\84ã\81\97ã\81¾ã\81\99ï¼\81",
        "feedback-useragent": "ユーザーエージェント:",
        "searchsuggest-search": "検索",
        "searchsuggest-containing": "この語句を全文検索",
index e51008c..79cea0e 100644 (file)
@@ -61,7 +61,7 @@
        "underline-always": "Sempre",
        "underline-never": "Mâi",
        "underline-default": "Impostassioin predefinie do navegatô o da skin",
-       "editfont-style": "Stile do carattere de l'aera de modiffica",
+       "editfont-style": "Stile do carattere de l'area de modiffica",
        "editfont-default": "Predefinio do navegatô",
        "editfont-monospace": "Carattere a larghessa fissa",
        "editfont-sansserif": "Carattere sans-serif",
        "undeletedrevisions": "{{PLURAL:$1|Una verscion recuperâ|$1 verscioin recuperæ}}",
        "undeletedrevisions-files": "{{PLURAL:$1|Una verscion|$1 verscioin}} e $2 file recuperæ",
        "undeletedfiles": "{{PLURAL:$1|Un file recuperou|$1 file recuperæ}}",
-       "cannotundelete": "Ripristino non riuscio:\n$1",
+       "cannotundelete": "Çerti ò tutti i ripristini non riuscii:\n$1",
        "undeletedpage": "'''A pagina $1 a l'è stæta recuperâ'''\n\nConsurta o [[Special:Log/delete|registro de scançellaçioin]] pe vedde e scançellaçioin e i recupperi ciù reçente.",
        "undelete-header": "Consurta o [[Special:Log/delete|registro de scançellaçioin]] pe vedde e scassatue ciù reçente.",
        "undelete-search-title": "Çerca inte pagine scassæ",
        "sp-contributions-newbies-sub": "Pe i nêuvi ûtenti",
        "sp-contributions-newbies-title": "Contribuçioin di noeuvi utenti",
        "sp-contributions-blocklog": "Blòcchi",
-       "sp-contributions-suppresslog": "contributi utente soppresci",
-       "sp-contributions-deleted": "contributi utente scassæ",
+       "sp-contributions-suppresslog": "contributi {{GENDER:$1|utente}} soppresci",
+       "sp-contributions-deleted": "contributi {{GENDER:$1|utente}}  scassæ",
        "sp-contributions-uploads": "caregaménti",
        "sp-contributions-logs": "log",
        "sp-contributions-talk": "Ciæti",
index 11dbc62..e528dde 100644 (file)
        "prefs-resetpass": "Mainīt paroli",
        "prefs-changeemail": "Mainīt vai noņemt e-pastu",
        "prefs-setemail": "Uzstādīt e-pasta adresi",
-       "prefs-email": "E-pasta uzstādījumi",
+       "prefs-email": "E-pasta iestatījumi",
        "prefs-rendering": "Izskats",
        "saveprefs": "Saglabāt",
        "restoreprefs": "Atjaunot noklusētos uzstādījumus (visās sadaļās)",
index 4d38ce7..562e3e5 100644 (file)
        "pageinfo-article-id": "The numeric identifier of the page.\n{{Identical|Page ID}}",
        "pageinfo-language": "Language in which the page content is written.",
        "pageinfo-content-model": "The model in which the page content is written.\n\nUsed as label at [{{fullurl:Main Page|action=info}} action=info]. Followed by one of the following messages:\n* {{msg-mw|Content-model-wikitext}}\n* {{msg-mw|Content-model-javascript}}\n* {{msg-mw|Content-model-css}}\n* {{msg-mw|Content-model-text}}",
+       "pageinfo-content-model-change": "Link text for a link to Special:ChangeContentModel. The link will be wrapped in parenthesis.",
        "pageinfo-robot-policy": "The search engine status of the page.\n\nUsed as label. Followed by any one of the following messages:\n*{{msg-mw|Pageinfo-robot-index}}\n*{{msg-mw|Pageinfo-robot-noindex}}",
        "pageinfo-robot-index": "An indication that the page is indexable by search engines, that is listed in their search results.\n\nPreceded by the label {{msg-mw|Pageinfo-robot-policy}}.\n{{Identical|Allowed}}",
        "pageinfo-robot-noindex": "An indication that the page is not indexable (that is, is not listed on the results page of a search engine).\n\nPreceded by the label {{msg-mw|Pageinfo-robot-policy}}.",
index ee6bff0..bb992d2 100644 (file)
        "yourpasswordagain": "ಪಾಸ್ವರ್ಡ್ ಪಿರ ಟೈಪ್ ಮಲ್ಪುಲೆ",
        "createacct-yourpasswordagain": "ಪ್ರವೇಸೊ ಪದೊನು ದೃಡೊ ಮಲ್ಪುಲೆ",
        "createacct-yourpasswordagain-ph": "ಪ್ರವೇಸೊ ಪದೊನು ನನ ಒರ ನಮೂದಿಸಲೆ",
-       "remembermypassword": "ಈ ಗಣಕಯಂತ್ರೊಡು ಎನ್ನ ಲಾಗಿನ್ ನೆಂಪು ದೀಡೊನ್ಲೆ(ಹೆಚ್ಚ್ $1 {{PLURAL:$1|ದಿನೊತ|ದಿನೊಕ್ಕುಲೆ}}ಮುಟ್ಟೊ)",
        "userlogin-remembermypassword": "ಎನನ್ ಲಾಗಿನ್ ಆತೇ ದೀಡ್ಲೆ",
        "userlogin-signwithsecure": "ರಕ್ಷಣೆದ ಕನೆಕ್ಷನ್ ಉಪಯೋಗಿಸಲೆ.",
        "cannotloginnow-title": "ಇತ್ತೆ ಉಲಾಯಿ ಪೋಯರ್ ಸಾದ್ಯೊ ಇದ್ದಿ",
        "passwordreset-username": "ಸದಸ್ಯೆರ್ನ ಪುದರ್:",
        "passwordreset-domain": "ಕ್ಷೇತ್ರೊ:",
        "passwordreset-email": "ಇ-ಅಂಚೆ ವಿಳಾಸೊ",
+       "passwordreset-invalideamil": "ಇಮೇಲ್ ಸರಿ ಇಜ್ಜಿ",
+       "changeemail-oldemail": "ಇತ್ತೆತಾ ಈಮೇಲ್ ವಿಳಾಸೊ:",
        "changeemail-newemail": "ಪೊಸ ಇ-ಅಂಚೆ ವಿಳಾಸೊ:",
        "changeemail-none": "ಒವ್ವುಲಾ ಇಜ್ಜಿ",
        "changeemail-submit": "ಇ-ಅಂಚೆ ವಿಳಾಸ ಬದಲಾವಣೆ ಮಲ್ಪುಲೆ",
        "permissionserrors": "ಅನುಮತಿ ದೋಷ",
        "permissionserrorstext-withaction": "$2 ಗ್ ಇರೆಗ್ ಅನುಮತಿ ಇದ್ದಿ, ಅಯಿಕ್ {{PLURAL:$1|ಕಾರಣೊ|ಕಾರಣೊಲು}}:",
        "moveddeleted-notice": "ಈ ಪುಟೊ ಅಸ್ತಿತ್ವೊಡ್ ಇದ್ದಿ.\nಪುಟೊದ ಡಿಲೀಶನ್ ಅತ್ತ್ಂಡ್ ಕಡಪ್ಪುಡುನೆ ಲಾಗ್‍ನ್ ತೂಯರೆ ತಿರ್ತ್ ಕೊರ್ತ್ಂಡ್.",
+       "postedit-confirmation-created": "ಈ ಪುಟೋನು ಉಂಡು ಮಾನ್ತುಂಡು.",
        "postedit-confirmation-saved": "ಇರೇನಾ ಸಂಪಾದನೆನ್ ಒರಿಪಾತುಂಡು.",
        "edit-already-exists": "ಪೊಸ ಪುಟೋನು ಉಂಡು ಮಲ್ಪರೆ ಅಯಿಜಿ. ಅವ್ವು ದುಂಬೇ ಉಂಡು.",
        "content-model-wikitext": "ವಿಕಿ ಪಠ್ಯ",
        "revdelete-hide-image": "ಪೈಲ್‘ಡ್  ಇಪ್ಪುನ ಮಾಹಿತ್‘ನ್ ದೆಂಗಾಲೆ",
        "revdelete-hide-name": "ಕಾರ್ಯ ಬೊಕ್ಕ ಗುರಿನ್ ದೆಂಗಾಲ",
        "revdelete-hide-comment": "ಸಾರಾಂಶ ಸಂಪೊಲಿಪುಲೆ",
+       "revdelete-radio-same": "(ಬದಲಾವಣೆ ಮಾಂಪಾಡ್ಚಿ)",
        "revdelete-radio-set": "ದೆಂಗಾಲೆ",
        "revdelete-radio-unset": "ತೋಜುಂಡು",
        "revdelete-log": "ಕಾರಣ",
        "right-delete": "ಪುಟೊಕುಲೆನ್ ಮಾಜಾಲೆ",
        "right-undelete": "ಪುಟೊನ್ ಮಾಜಾವಡೆ",
        "grant-group-email": "ಇ-ಅಂಚೆ ಕಡಪುಡುಲೆ",
+       "grant-createaccount": "ಪೊಸ ಕಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
        "newuserlogpage": "ಸದಸ್ಯೆರೆ ಸ್ರಿಸ್ಟಿದ ದಾಕಲೆ",
        "rightslog": "ಸದಸ್ಯೆರ್ನ ಹಕ್ಕು ದಾಖಲೆ",
        "action-read": "ಈ ಪುಟೊನು ಓದುಲೆ",
        "action-upload": "ಈ ಫೈಲ್‘ನ್ ಅಪ್‘ಲೋಡ್ ಮಲ್ಪುಲೆ",
        "action-delete": "ಈ ಪುಟೊನ್ ಮಾಜಾಲೆ",
        "action-deleterevision": "ಈ ಆವೃತ್ತಿನ್ ಮಾಜಾಲೆ",
+       "action-browsearchive": "ಮಜಾಯಿನಾ ಪುಟೋನ್ ನಡ್ಲೆ",
+       "action-undelete": "ಈ ಪುಟೊನ್ ಮಾಜಾಯಿನೆನ್ ರದ್ದ್ ಮಾನ್ಪುಲೇ",
        "action-sendemail": "ಇ-ಅಂಚೆ ಕಡಪುಡುಲೆ",
        "nchanges": "$1 {{PLURAL:$1|ಬದಲಾವಣೆ|ಬದಲಾವಣೆಲು}}",
        "enhancedrc-history": "ಇತಿಹಾಸೊ",
        "upload-disallowed-here": "ಈರ್ ಈ ಫೈಲ್‍ನ್ ಕುಡೊರೊ ಬರೆವರೆ ಸಾದ್ಯೊ ಇದ್ದಿ.",
        "filerevert-comment": "ಕಾರಣ:",
        "filerevert-submit": "ದುಂಬುದ ಲೆಕ ಮಲ್ಪುಲೆ",
+       "filedelete": "$1 ನ್ ಮಾಜಾಲೆ",
        "filedelete-legend": "ಕಡತನ್ ಮಾಜಾಲೆ",
        "filedelete-comment": "ಕಾರಣ",
        "filedelete-submit": "ಮಾಜಾಲೆ",
        "protectedpages": "ಸಂರಕ್ಷಿತ ಪುಟೊ",
        "protectedpages-page": "ಪುಟೊ",
        "protectedpages-reason": "ಕಾರಣೊ",
+       "protectedpages-submit": "ಪ್ರದರ್ಶಿಶಿಸಾಯಿನ ಪುದರ್",
        "protectedpages-unknown-timestamp": "ಗೊತ್ತಿಜ್ಜಾಂದಿನ",
        "protectedpages-unknown-performer": "ಅಜ್ಞಾತ ಬಳಕೆದಾರೆ",
        "protectedtitles": "ಸಂರಕ್ಷಿತ ಶೀರ್ಷಿಕೆಲು",
index da75ac8..78b2457 100644 (file)
        "generic-pool-error": "ہم معذرت خواہ ہیں! معیلات (سرورز) پر اِس وقت اِضافی بوجھ ہے.\nصارفین کی کثیر تعداد اِس وقت یہی صفحہ ملاحظہ کرنے کی کوشش کررہی ہے.\nبرائے مہربانی!دوبارہ کوشش کرنے سے پہلے ذرا انتظار فرمائیے.",
        "pool-errorunknown": "نامعلوم خطا",
        "poolcounter-usage-error": "استعمال میں خامی: $1",
-       "aboutsite": "تعارف {{SITENAME}}",
+       "aboutsite": "{{SITENAME}} کا تعارف",
        "aboutpage": "Project:تعارف",
        "copyright": "تمام مواد $1 کے تحت میسر ہے، جب تک کوئی دوسری وجہ نا ہو۔",
        "copyrightpage": "{{ns:project}}:حقوق تصانیف",
        "policy-url": "Project:حکمتِ عملی",
        "portal": "دیوان عام",
        "portal-url": "Project:دیوان عام",
-       "privacy": "اصÙ\88Ù\84 Ø¨Ø±Ø§Û\93 Ø§Ø®Ù\81ائÛ\92 Ø±Ø§Ø²",
+       "privacy": "اخÙ\81ائÛ\92 Ø±Ø§Ø² Ú©Û\92 Ø§ØµÙ\88Ù\84",
        "privacypage": "Project:اصولِ اخفائے راز",
        "badaccess": "خطائے اجازت",
        "badaccess-group0": "آپ متمنی عمل کا اجراء کرنے کے مُجاز نہیں۔",
        "history-feed-description": "ویکی پر اِس صفحہ کا تاریخچۂ نظرثانی",
        "history-feed-item-nocomment": "بہ $2 $1",
        "history-feed-empty": "درخواست شدہ صفحہ موجود نہیں.\nیا تو یہ ویکی سے حذف کیا گیا ہے اور یا اِس کا نام تبدیل کردیا گیا ہے.\nآپ متعلقہ نئے صفحات کیلئے [[Special:Search|ویکی پر تلاش]] کرسکتے ہیں.",
+       "history-edit-tags": "منتخب نظرثانیوں کے ٹیگوں میں ترمیم کریں",
        "rev-deleted-comment": "(تبصرہ حذف کی گيا ہے)",
        "rev-deleted-user": "(صارف نام حذف کیا گيا ہے)",
        "rev-delundel": "دکھاؤ/چھپاؤ",
        "speciallogtitlelabel": "ہدف (عنوان یا {{ns:user}}:صارف نام برائے صارف):",
        "log": "نوشتہ جات",
        "logeventslist-submit": "دکھائیں",
+       "checkbox-select": "$1 کو منتخب کریں",
+       "checkbox-all": "سب",
+       "checkbox-none": "کچھ نہیں",
+       "checkbox-invert": "برعکس",
        "allpages": "تمام صفحات",
        "nextpage": "اگلا صفحہ ($1)",
        "prevpage": "پچھلا صفحہ ($1)",
        "undelete-search-submit": "تلاش",
        "undelete-no-results": "حذف شدہ صفحات میں ایسا کوئی صفحہ نہیں ملا",
        "undelete-show-file-submit": "ہاں",
-       "namespace": "Ù\81ضائÛ\92 Ù\86اÙ\85:",
+       "namespace": "Ù\86اÙ\85 Ù\81ضا:",
        "invert": "انتخاب بالعکس",
        "tooltip-invert": "منتخب شدہ فضائے نام (اور مُلحقہ فضائے نام) میں شامل صفحات کی تبدیلیوں کو چُھپانے کیلئے اِس خانہ کو ٹِک کریں۔",
-       "namespace_association": "متعلقہ فضا",
+       "namespace_association": "Ù\85تعÙ\84Ù\82Û\81 Ù\86اÙ\85 Ù\81ضا",
        "blanknamespace": "(مرکز)",
        "contributions": "{{GENDER:$1|صارف}} شراکتیں",
        "contributions-title": "مساہماتِ صارف برائے $1",
index d32c977..31b213f 100644 (file)
        "file-thumbnail-no": "Tên tập tin bắt đầu bằng <strong>$1</strong>.\nCó vẻ đây là bản thu nhỏ của hình gốc ''(thumbnail)''.\nNếu bạn có hình ở độ phân giải tối đa, xin hãy tải bản đó lên, nếu không xin hãy đổi lại tên tập tin.",
        "fileexists-forbidden": "Đã có tập tin với tên gọi này, và nó không thể bị ghi đè.\nNếu bạn vẫn muốn tải tập tin của bạn lên, xin hãy quay lại và sử dụng một tên khác. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Một tập tin với tên này đã tồn tại ở kho tập tin dùng chung.\nNếu bạn vẫn muốn tải tập tin của bạn lên, xin hãy quay lại và dùng một tên khác. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Tập tin tải lên này là bản sao y hệt với phiên bản hiện tại của <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "Tập tin tải lên này là bản sao y hệt với {{PLURAL:$2|một phiên bản trước đây|các phiên bản trước đây}} của <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Tập tin này có vẻ là bản sao của {{PLURAL:$1|tập tin|các  tập tin}} sau:",
        "file-deleted-duplicate": "Một tập tin giống hệt như tập tin này ([[:$1]]) đã từng bị xóa trước đây. Bạn nên xem lại lịch sử xóa tập tin trước khi tiếp tục tải nó lên lại.",
        "file-deleted-duplicate-notitle": "Một tập tin giống hệt như tập tin này đã từng bị xóa và tên bị xóa hẳn trước đây.\nBạn nên xin một người có quyền xem dữ liệu tập tin bị xóa hẳn xem lại trường hợp này trước khi tiếp tục tải nó lên lại.",
index 938bac5..f1f164d 100644 (file)
        "nrevisions": "$1 次修訂",
        "nimagelinks": "被 $1 個頁面使用",
        "ntransclusions": "被 $1 個頁面使用",
-       "specialpage-empty": "此報表查無任何結果。",
+       "specialpage-empty": "此報表查無任何結果。",
        "lonelypages": "孤立頁面",
        "lonelypagestext": "下列頁面尚未被 {{SITENAME}} 中的其它頁面連結或引用。",
        "uncategorizedpages": "未分類的頁面",
index eb2b3fc..cdc4dbf 100644 (file)
@@ -1,6 +1,6 @@
 /**
  * @class mw.Api.plugin.rollback
- * @since 1.27
+ * @since 1.28
  */
 ( function ( mw, $ ) {
 
index 1ff62ba..83d14b3 100644 (file)
@@ -2,7 +2,7 @@
  * Enhance rollback links by using asynchronous API requests,
  * rather than navigating to an action page.
  *
- * @since 1.27
+ * @since 1.28
  * @author Timo Tijhof
  */
 ( function ( mw, $ ) {
index 5488280..4858703 100644 (file)
@@ -26,18 +26,6 @@ $testDir = __DIR__;
 
 $wgAutoloadClasses += [
 
-       # tests
-       'DbTestPreviewer' => "$testDir/testHelpers.inc",
-       'DbTestRecorder' => "$testDir/testHelpers.inc",
-       'DelayedParserTest' => "$testDir/testHelpers.inc",
-       'ParserTestResult' => "$testDir/parser/ParserTestResult.php",
-       'TestFileIterator' => "$testDir/testHelpers.inc",
-       'TestFileDataProvider' => "$testDir/testHelpers.inc",
-       'TestRecorder' => "$testDir/testHelpers.inc",
-       'ITestRecorder' => "$testDir/testHelpers.inc",
-       'DjVuSupport' => "$testDir/testHelpers.inc",
-       'TidySupport' => "$testDir/testHelpers.inc",
-
        # tests/phpunit
        'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
        'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
@@ -142,11 +130,21 @@ $wgAutoloadClasses += [
        'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
 
        # tests/parser
-       'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php",
+       '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",
-       'ParserTest' => "$testDir/parser/parserTest.inc",
-       'ParserTestResultNormalizer' => "$testDir/parser/parserTest.inc",
-       'ParserTestParserHook' => "$testDir/parser/parserTestsParserHook.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",
index 8740e5d..3f451f1 100644 (file)
@@ -21,6 +21,7 @@ mw-vagrant-host: &default
 mw-vagrant-guest:
   user_factory: true
   mediawiki_url: http://127.0.0.1/wiki/
+  headless: 'true'
 
 beta:
   mediawiki_url: https://en.wikipedia.beta.wmflabs.org/wiki/
diff --git a/tests/parser/DbTestPreviewer.php b/tests/parser/DbTestPreviewer.php
new file mode 100644 (file)
index 0000000..2412254
--- /dev/null
@@ -0,0 +1,228 @@
+<?php
+/**
+ * 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
+ */
+
+class DbTestPreviewer extends TestRecorder {
+       protected $lb; // /< Database load balancer
+       protected $db; // /< Database connection to the main DB
+       protected $curRun; // /< run ID number for the current run
+       protected $prevRun; // /< run ID number for the previous run, if any
+       protected $results; // /< Result array
+
+       /**
+        * This should be called before the table prefix is changed
+        * @param TestRecorder $parent
+        */
+       function __construct( $parent ) {
+               parent::__construct( $parent );
+
+               $this->lb = wfGetLBFactory()->newMainLB();
+               // This connection will have the wiki's table prefix, not parsertest_
+               $this->db = $this->lb->getConnection( DB_MASTER );
+       }
+
+       /**
+        * Set up result recording; insert a record for the run with the date
+        * and all that fun stuff
+        */
+       function start() {
+               parent::start();
+
+               if ( !$this->db->tableExists( 'testrun', __METHOD__ )
+                       || !$this->db->tableExists( 'testitem', __METHOD__ )
+               ) {
+                       print "WARNING> `testrun` table not found in database.\n";
+                       $this->prevRun = false;
+               } else {
+                       // We'll make comparisons against the previous run later...
+                       $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
+               }
+
+               $this->results = [];
+       }
+
+       function getName( $test, $subtest ) {
+               if ( $subtest ) {
+                       return "$test subtest #$subtest";
+               } else {
+                       return $test;
+               }
+       }
+
+       function record( $test, $subtest, $result ) {
+               parent::record( $test, $subtest, $result );
+               $this->results[ $this->getName( $test, $subtest ) ] = $result;
+       }
+
+       function report() {
+               if ( $this->prevRun ) {
+                       // f = fail, p = pass, n = nonexistent
+                       // codes show before then after
+                       $table = [
+                               'fp' => 'previously failing test(s) now PASSING! :)',
+                               'pn' => 'previously PASSING test(s) removed o_O',
+                               'np' => 'new PASSING test(s) :)',
+
+                               'pf' => 'previously passing test(s) now FAILING! :(',
+                               'fn' => 'previously FAILING test(s) removed O_o',
+                               'nf' => 'new FAILING test(s) :(',
+                               'ff' => 'still FAILING test(s) :(',
+                       ];
+
+                       $prevResults = [];
+
+                       $res = $this->db->select( 'testitem', [ 'ti_name', 'ti_success' ],
+                               [ 'ti_run' => $this->prevRun ], __METHOD__ );
+
+                       foreach ( $res as $row ) {
+                               if ( !$this->parent->regex
+                                       || preg_match( "/{$this->parent->regex}/i", $row->ti_name )
+                               ) {
+                                       $prevResults[$row->ti_name] = $row->ti_success;
+                               }
+                       }
+
+                       $combined = array_keys( $this->results + $prevResults );
+
+                       # Determine breakdown by change type
+                       $breakdown = [];
+                       foreach ( $combined as $test ) {
+                               if ( !isset( $prevResults[$test] ) ) {
+                                       $before = 'n';
+                               } elseif ( $prevResults[$test] == 1 ) {
+                                       $before = 'p';
+                               } else /* if ( $prevResults[$test] == 0 )*/ {
+                                       $before = 'f';
+                               }
+
+                               if ( !isset( $this->results[$test] ) ) {
+                                       $after = 'n';
+                               } elseif ( $this->results[$test] == 1 ) {
+                                       $after = 'p';
+                               } else /*if ( $this->results[$test] == 0 ) */ {
+                                       $after = 'f';
+                               }
+
+                               $code = $before . $after;
+
+                               if ( isset( $table[$code] ) ) {
+                                       $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
+                               }
+                       }
+
+                       # Write out results
+                       foreach ( $table as $code => $label ) {
+                               if ( !empty( $breakdown[$code] ) ) {
+                                       $count = count( $breakdown[$code] );
+                                       printf( "\n%4d %s\n", $count, $label );
+
+                                       foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
+                                               print "      * $differing_test_name  [$statusInfo]\n";
+                                       }
+                               }
+                       }
+               } else {
+                       print "No previous test runs to compare against.\n";
+               }
+
+               print "\n";
+               parent::report();
+       }
+
+       /**
+        * Returns a string giving information about when a test last had a status change.
+        * Could help to track down when regressions were introduced, as distinct from tests
+        * which have never passed (which are more change requests than regressions).
+        * @param string $testname
+        * @param string $after
+        * @return string
+        */
+       private function getTestStatusInfo( $testname, $after ) {
+               // If we're looking at a test that has just been removed, then say when it first appeared.
+               if ( $after == 'n' ) {
+                       $changedRun = $this->db->selectField( 'testitem',
+                               'MIN(ti_run)',
+                               [ 'ti_name' => $testname ],
+                               __METHOD__ );
+                       $appear = $this->db->selectRow( 'testrun',
+                               [ 'tr_date', 'tr_mw_version' ],
+                               [ 'tr_id' => $changedRun ],
+                               __METHOD__ );
+
+                       return "First recorded appearance: "
+                               . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
+                               . ", " . $appear->tr_mw_version;
+               }
+
+               // Otherwise, this test has previous recorded results.
+               // See when this test last had a different result to what we're seeing now.
+               $conds = [
+                       'ti_name' => $testname,
+                       'ti_success' => ( $after == 'f' ? "1" : "0" ) ];
+
+               if ( $this->curRun ) {
+                       $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
+               }
+
+               $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
+
+               // If no record of ever having had a different result.
+               if ( is_null( $changedRun ) ) {
+                       if ( $after == "f" ) {
+                               return "Has never passed";
+                       } else {
+                               return "Has never failed";
+                       }
+               }
+
+               // Otherwise, we're looking at a test whose status has changed.
+               // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
+               // In this situation, give as much info as we can as to when it changed status.
+               $pre = $this->db->selectRow( 'testrun',
+                       [ 'tr_date', 'tr_mw_version' ],
+                       [ 'tr_id' => $changedRun ],
+                       __METHOD__ );
+               $post = $this->db->selectRow( 'testrun',
+                       [ 'tr_date', 'tr_mw_version' ],
+                       [ "tr_id > " . $this->db->addQuotes( $changedRun ) ],
+                       __METHOD__,
+                       [ "LIMIT" => 1, "ORDER BY" => 'tr_id' ]
+               );
+
+               if ( $post ) {
+                       $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
+               } else {
+                       $postDate = 'now';
+               }
+
+               return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
+                       . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
+                       . " and $postDate";
+       }
+
+       /**
+        * Close the DB connection
+        */
+       function end() {
+               $this->lb->closeAll();
+               parent::end();
+       }
+}
+
diff --git a/tests/parser/DbTestRecorder.php b/tests/parser/DbTestRecorder.php
new file mode 100644 (file)
index 0000000..26aef97
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * 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
+ */
+
+class DbTestRecorder extends DbTestPreviewer {
+       public $version;
+
+       /**
+        * Set up result recording; insert a record for the run with the date
+        * and all that fun stuff
+        */
+       function start() {
+               $this->db->begin( __METHOD__ );
+
+               if ( !$this->db->tableExists( 'testrun' )
+                       || !$this->db->tableExists( 'testitem' )
+               ) {
+                       print "WARNING> `testrun` table not found in database. Trying to create table.\n";
+                       $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) );
+                       echo "OK, resuming.\n";
+               }
+
+               parent::start();
+
+               $this->db->insert( 'testrun',
+                       [
+                               'tr_date' => $this->db->timestamp(),
+                               'tr_mw_version' => $this->version,
+                               'tr_php_version' => PHP_VERSION,
+                               'tr_db_version' => $this->db->getServerVersion(),
+                               'tr_uname' => php_uname()
+                       ],
+                       __METHOD__ );
+               if ( $this->db->getType() === 'postgres' ) {
+                       $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
+               } else {
+                       $this->curRun = $this->db->insertId();
+               }
+       }
+
+       /**
+        * Record an individual test item's success or failure to the db
+        *
+        * @param string $test
+        * @param bool $result
+        */
+       function record( $test, $subtest, $result ) {
+               parent::record( $test, $subtest, $result );
+
+               $this->db->insert( 'testitem',
+                       [
+                               'ti_run' => $this->curRun,
+                               'ti_name' => $this->getName( $test, $subtest ),
+                               'ti_success' => $result ? 1 : 0,
+                       ],
+                       __METHOD__ );
+       }
+
+       /**
+        * Commit transaction and clean up for result recording
+        */
+       function end() {
+               $this->db->commit( __METHOD__ );
+               parent::end();
+       }
+}
+
diff --git a/tests/parser/DelayedParserTest.php b/tests/parser/DelayedParserTest.php
new file mode 100644 (file)
index 0000000..1c5c36b
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * 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
+ */
+
+/**
+ * A class to delay execution of a parser test hooks.
+ */
+class DelayedParserTest {
+
+       /** Initialized on construction */
+       private $hooks;
+       private $fnHooks;
+       private $transparentHooks;
+
+       public function __construct() {
+               $this->reset();
+       }
+
+       /**
+        * Init/reset or forgot about the current delayed test.
+        * Call to this will erase any hooks function that were pending.
+        */
+       public function reset() {
+               $this->hooks = [];
+               $this->fnHooks = [];
+               $this->transparentHooks = [];
+       }
+
+       /**
+        * 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
+        * @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" );
+               }
+
+               # Trigger delayed hooks. Any failure will make us abort
+               foreach ( $this->hooks as $hook ) {
+                       $ret = $parserTest->requireHook( $hook );
+                       if ( !$ret ) {
+                               return false;
+                       }
+               }
+
+               # Trigger delayed function hooks. Any failure will make us abort
+               foreach ( $this->fnHooks as $fnHook ) {
+                       $ret = $parserTest->requireFunctionHook( $fnHook );
+                       if ( !$ret ) {
+                               return false;
+                       }
+               }
+
+               # Trigger delayed transparent hooks. Any failure will make us abort
+               foreach ( $this->transparentHooks as $hook ) {
+                       $ret = $parserTest->requireTransparentHook( $hook );
+                       if ( !$ret ) {
+                               return false;
+                       }
+               }
+
+               # Delayed execution was successful.
+               return true;
+       }
+
+       /**
+        * Similar to ParserTest object but does not run anything
+        * Use unleash() to really execute the hook
+        * @param string $hook
+        */
+       public function requireHook( $hook ) {
+               $this->hooks[] = $hook;
+       }
+
+       /**
+        * Similar to ParserTest object but does not run anything
+        * Use unleash() to really execute the hook function
+        * @param string $fnHook
+        */
+       public function requireFunctionHook( $fnHook ) {
+               $this->fnHooks[] = $fnHook;
+       }
+
+       /**
+        * Similar to ParserTest object but does not run anything
+        * Use unleash() to really execute the hook function
+        * @param string $hook
+        */
+       public function requireTransparentHook( $hook ) {
+               $this->transparentHooks[] = $hook;
+       }
+
+}
+
diff --git a/tests/parser/DjVuSupport.php b/tests/parser/DjVuSupport.php
new file mode 100644 (file)
index 0000000..4739be4
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * 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
+ */
+
+/**
+ * Initialize and detect the DjVu files support
+ */
+class DjVuSupport {
+
+       /**
+        * Initialises DjVu tools global with default values
+        */
+       public function __construct() {
+               global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
+
+               $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
+               $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
+               $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
+               $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
+
+               if ( !in_array( 'djvu', $wgFileExtensions ) ) {
+                       $wgFileExtensions[] = 'djvu';
+               }
+       }
+
+       /**
+        * Returns true if the DjVu tools are usable
+        *
+        * @return bool
+        */
+       public function isEnabled() {
+               global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
+
+               return is_executable( $wgDjvuRenderer )
+                       && is_executable( $wgDjvuDump )
+                       && is_executable( $wgDjvuToXML )
+                       && is_executable( $wgDjvuTxt );
+       }
+}
+
diff --git a/tests/parser/ITestRecorder.php b/tests/parser/ITestRecorder.php
new file mode 100644 (file)
index 0000000..5a78beb
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * 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
+ */
+
+/**
+ * Interface to record parser test results.
+ *
+ * The ITestRecorder is a very simple interface to record the result of
+ * MediaWiki parser tests. One should call start() before running the
+ * full parser tests and end() once all the tests have been finished.
+ * After each test, you should use record() to keep track of your tests
+ * results. Finally, report() is used to generate a summary of your
+ * test run, one could dump it to the console for human consumption or
+ * register the result in a database for tracking purposes.
+ *
+ * @since 1.22
+ */
+interface ITestRecorder {
+
+       /**
+        * Called at beginning of the parser test run
+        */
+       public function start();
+
+       /**
+        * Called after each test
+        * @param string $test
+        * @param integer $subtest
+        * @param bool $result
+        */
+       public function record( $test, $subtest, $result );
+
+       /**
+        * Called before finishing the test run
+        */
+       public function report();
+
+       /**
+        * Called at the end of the parser test run
+        */
+       public function end();
+
+}
+
diff --git a/tests/parser/ParserTest.php b/tests/parser/ParserTest.php
new file mode 100644 (file)
index 0000000..7b3746a
--- /dev/null
@@ -0,0 +1,1591 @@
+<?php
+/**
+ * Helper code for the MediaWiki parser test suite. Some code is duplicated
+ * in PHPUnit's NewParserTests.php, so you'll probably want to update both
+ * at the same time.
+ *
+ * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com>
+ * 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)
+                       (?<qstr>                                        # Quoted string
+                               "
+                               (?:[^\\\\"] | \\\\.)*
+                               "
+                       )
+                       (?<json>
+                               \{              # Open bracket
+                               (?:
+                                       [^"{}] |                                # Not a quoted string or object, or
+                                       (?&qstr) |                              # A quoted string, or
+                                       (?&json)                                # A json object (recursively)
+                               )*
+                               \}              # Close bracket
+                       )
+                       (?<value>
+                               (?:
+                                       (?&qstr)                        # Quoted val
+                               |
+                                       \[\[
+                                               [^]]*                   # Link target
+                                       \]\]
+                               |
+                                       [\w-]+                          # Plain word
+                               |
+                                       (?&json)                        # JSON object
+                               )
+                       )
+               )';
+               $regex = '/' . $defs . '\b
+                       (?<k>[\w-]+)                            # Key
+                       \b
+                       (?:\s*
+                               =                                               # First sub-value
+                               \s*
+                               (?<v>
+                                       (?&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' => '<?xml version="1.0" ?>
+<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
+<DjVuXML>
+<HEAD></HEAD>
+<BODY><OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+</BODY>
+</DjVuXML>',
+                       '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",
+                       '<?xml version="1.0" encoding="utf-8"?>' .
+                       '<svg xmlns="http://www.w3.org/2000/svg"' .
+                       ' version="1.1" width="240" height="180"/>' );
+               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() .
+                               '<html>' .
+                               $text .
+                               '</html>';
+
+               $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/ParserTestParserHook.php b/tests/parser/ParserTestParserHook.php
new file mode 100644 (file)
index 0000000..5bf50ea
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * A basic extension that's used by the parser tests to test whether input and
+ * arguments are passed to extensions properly.
+ *
+ * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
+ *
+ * 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
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ */
+
+class ParserTestParserHook {
+
+       static function setup( &$parser ) {
+               $parser->setHook( 'tag', [ __CLASS__, 'dumpHook' ] );
+               $parser->setHook( 'tåg', [ __CLASS__, 'dumpHook' ] );
+               $parser->setHook( 'statictag', [ __CLASS__, 'staticTagHook' ] );
+               return true;
+       }
+
+       static function dumpHook( $in, $argv ) {
+               return "<pre>\n" .
+                       var_export( $in, true ) . "\n" .
+                       var_export( $argv, true ) . "\n" .
+                       "</pre>";
+       }
+
+       static function staticTagHook( $in, $argv, $parser ) {
+               if ( !count( $argv ) ) {
+                       $parser->static_tag_buf = $in;
+                       return '';
+               } elseif ( count( $argv ) === 1 && isset( $argv['action'] )
+                       && $argv['action'] === 'flush' && $in === null
+               ) {
+                       // Clear the buffer, we probably don't need to
+                       if ( isset( $parser->static_tag_buf ) ) {
+                               $tmp = $parser->static_tag_buf;
+                       } else {
+                               $tmp = '';
+                       }
+                       $parser->static_tag_buf = null;
+                       return $tmp;
+               } else { // wtf?
+                       return
+                               "\nCall this extension as <statictag>string</statictag> or as" .
+                               " <statictag action=flush/>, not in any other way.\n" .
+                               "text: " . var_export( $in, true ) . "\n" .
+                               "argv: " . var_export( $argv, true ) . "\n";
+               }
+       }
+}
diff --git a/tests/parser/ParserTestResultNormalizer.php b/tests/parser/ParserTestResultNormalizer.php
new file mode 100644 (file)
index 0000000..a15d09e
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/**
+ * @file
+ * @ingroup Testing
+ */
+
+class ParserTestResultNormalizer {
+       protected $doc, $xpath, $invalid;
+
+       public static function normalize( $text, $funcs ) {
+               $norm = new self( $text );
+               if ( $norm->invalid ) {
+                       return $text;
+               }
+               foreach ( $funcs as $func ) {
+                       $norm->$func();
+               }
+               return $norm->serialize();
+       }
+
+       protected function __construct( $text ) {
+               $this->doc = new DOMDocument( '1.0', 'utf-8' );
+
+               // Note: parsing a supposedly XHTML document with an XML parser is not
+               // guaranteed to give accurate results. For example, it may introduce
+               // differences in the number of line breaks in <pre> tags.
+
+               MediaWiki\suppressWarnings();
+               if ( !$this->doc->loadXML( '<html><body>' . $text . '</body></html>' ) ) {
+                       $this->invalid = true;
+               }
+               MediaWiki\restoreWarnings();
+               $this->xpath = new DOMXPath( $this->doc );
+               $this->body = $this->xpath->query( '//body' )->item( 0 );
+       }
+
+       protected function removeTbody() {
+               foreach ( $this->xpath->query( '//tbody' ) as $tbody ) {
+                       while ( $tbody->firstChild ) {
+                               $child = $tbody->firstChild;
+                               $tbody->removeChild( $child );
+                               $tbody->parentNode->insertBefore( $child, $tbody );
+                       }
+                       $tbody->parentNode->removeChild( $tbody );
+               }
+       }
+
+       /**
+        * The point of this function is to produce a normalized DOM in which
+        * Tidy's output matches the output of html5depurate. Tidy both trims
+        * and pretty-prints, so this requires fairly aggressive treatment.
+        *
+        * In particular, note that Tidy converts <pre>x</pre> to <pre>\nx\n</pre>,
+        * which theoretically affects display since the second line break is not
+        * ignored by compliant HTML parsers.
+        *
+        * This function also removes empty elements, as does Tidy.
+        */
+       protected function trimWhitespace() {
+               foreach ( $this->xpath->query( '//text()' ) as $child ) {
+                       if ( strtolower( $child->parentNode->nodeName ) === 'pre' ) {
+                               // Just trim one line break from the start and end
+                               if ( substr_compare( $child->data, "\n", 0 ) === 0 ) {
+                                       $child->data = substr( $child->data, 1 );
+                               }
+                               if ( substr_compare( $child->data, "\n", -1 ) === 0 ) {
+                                       $child->data = substr( $child->data, 0, -1 );
+                               }
+                       } else {
+                               // Trim all whitespace
+                               $child->data = trim( $child->data );
+                       }
+                       if ( $child->data === '' ) {
+                               $child->parentNode->removeChild( $child );
+                       }
+               }
+       }
+
+       /**
+        * Serialize the XML DOM for comparison purposes. This does not generate HTML.
+        */
+       protected function serialize() {
+               return strtr( $this->doc->saveXML( $this->body ),
+                       [ '<body>' => '', '</body>' => '' ] );
+       }
+}
+
diff --git a/tests/parser/TestFileDataProvider.php b/tests/parser/TestFileDataProvider.php
new file mode 100644 (file)
index 0000000..00b1f3f
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * 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
+ */
+
+/**
+ * An iterator for use as a phpunit data provider. Provides the test arguments
+ * in the order expected by NewParserTest::testParserTest().
+ */
+class TestFileDataProvider extends TestFileIterator {
+       function current() {
+               $test = parent::current();
+               if ( $test ) {
+                       return [
+                               $test['test'],
+                               $test['input'],
+                               $test['result'],
+                               $test['options'],
+                               $test['config'],
+                       ];
+               } else {
+                       return $test;
+               }
+       }
+}
+
diff --git a/tests/parser/TestFileIterator.php b/tests/parser/TestFileIterator.php
new file mode 100644 (file)
index 0000000..731d35c
--- /dev/null
@@ -0,0 +1,324 @@
+<?php
+/**
+ * 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
+ */
+
+class TestFileIterator implements Iterator {
+       private $file;
+       private $fh;
+       /**
+        * @var ParserTest|MediaWikiParserTest An instance of ParserTest (parserTests.php)
+        *  or MediaWikiParserTest (phpunit)
+        */
+       private $parserTest;
+       private $index = 0;
+       private $test;
+       private $section = null;
+       /** String|null: current test section being analyzed */
+       private $sectionData = [];
+       private $lineNum;
+       private $eof;
+       # Create a fake parser tests which never run anything unless
+       # asked to do so. This will avoid running hooks for a disabled test
+       private $delayedParserTest;
+       private $nextSubTest = 0;
+
+       function __construct( $file, $parserTest ) {
+               $this->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/TestRecorder.php b/tests/parser/TestRecorder.php
new file mode 100644 (file)
index 0000000..2608420
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * 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
+ */
+
+class TestRecorder implements ITestRecorder {
+       public $parent;
+       public $term;
+
+       function __construct( $parent ) {
+               $this->parent = $parent;
+               $this->term = $parent->term;
+       }
+
+       function start() {
+               $this->total = 0;
+               $this->success = 0;
+       }
+
+       function record( $test, $subtest, $result ) {
+               $this->total++;
+               $this->success += ( $result ? 1 : 0 );
+       }
+
+       function end() {
+               // dummy
+       }
+
+       function report() {
+               if ( $this->total > 0 ) {
+                       $this->reportPercentage( $this->success, $this->total );
+               } else {
+                       throw new MWException( "No tests found.\n" );
+               }
+       }
+
+       function reportPercentage( $success, $total ) {
+               $ratio = wfPercent( 100 * $success / $total );
+               print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
+
+               if ( $success == $total ) {
+                       print $this->term->color( 32 ) . "ALL TESTS PASSED!";
+               } else {
+                       $failed = $total - $success;
+                       print $this->term->color( 31 ) . "$failed tests failed!";
+               }
+
+               print $this->term->reset() . "\n";
+
+               return ( $success == $total );
+       }
+}
+
diff --git a/tests/parser/TidySupport.php b/tests/parser/TidySupport.php
new file mode 100644 (file)
index 0000000..6b5fb48
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * 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
+ */
+
+/**
+ * Initialize and detect the tidy support
+ */
+class TidySupport {
+       private $enabled;
+       private $config;
+
+       /**
+        * Determine if there is a usable tidy.
+        */
+       public function __construct( $useConfiguration = false ) {
+               global $IP, $wgUseTidy, $wgTidyBin, $wgTidyInternal, $wgTidyConfig,
+                       $wgTidyConf, $wgTidyOpts;
+
+               $this->enabled = true;
+               if ( $useConfiguration ) {
+                       if ( $wgTidyConfig !== null ) {
+                               $this->config = $wgTidyConfig;
+                       } elseif ( $wgUseTidy ) {
+                               $this->config = [
+                                       'tidyConfigFile' => $wgTidyConf,
+                                       'debugComment' => false,
+                                       'tidyBin' => $wgTidyBin,
+                                       'tidyCommandLine' => $wgTidyOpts
+                               ];
+                               if ( $wgTidyInternal ) {
+                                       $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP';
+                               } else {
+                                       $this->config['driver'] = 'RaggettExternal';
+                               }
+                       } else {
+                               $this->enabled = false;
+                       }
+               } else {
+                       $this->config = [
+                               'tidyConfigFile' => "$IP/includes/tidy/tidy.conf",
+                               'tidyCommandLine' => '',
+                       ];
+                       if ( extension_loaded( 'tidy' ) && class_exists( 'tidy' ) ) {
+                               $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP';
+                       } else {
+                               if ( is_executable( $wgTidyBin ) ) {
+                                       $this->config['driver'] = 'RaggettExternal';
+                                       $this->config['tidyBin'] = $wgTidyBin;
+                               } else {
+                                       $path = Installer::locateExecutableInDefaultPaths( $wgTidyBin );
+                                       if ( $path !== false ) {
+                                               $this->config['driver'] = 'RaggettExternal';
+                                               $this->config['tidyBin'] = $wgTidyBin;
+                                       } else {
+                                               $this->enabled = false;
+                                       }
+                               }
+                       }
+               }
+               if ( !$this->enabled ) {
+                       $this->config = [ 'driver' => 'disabled' ];
+               }
+       }
+
+       /**
+        * Returns true if tidy is usable
+        *
+        * @return bool
+        */
+       public function isEnabled() {
+               return $this->enabled;
+       }
+
+       public function getConfig() {
+               return $this->config;
+       }
+}
diff --git a/tests/parser/fuzzTest.php b/tests/parser/fuzzTest.php
new file mode 100644 (file)
index 0000000..045a770
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+
+require __DIR__ . '/../../maintenance/Maintenance.php';
+
+// Make RequestContext::resetMain() happy
+define( 'MW_PARSER_TEST', 1 );
+
+class ParserFuzzTest extends Maintenance {
+       private $parserTest;
+       private $maxFuzzTestLength = 300;
+       private $memoryLimit = 100;
+       private $seed;
+
+       function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Run a fuzz test on the parser, until it segfaults ' .
+                       'or throws an exception' );
+               $this->addOption( 'file', 'Use the specified file as a dictionary, ' .
+                       ' or leave blank to use parserTests.txt', false, true, true );
+
+               $this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
+       }
+
+       function finalSetup() {
+               require_once __DIR__ . '/../TestsAutoLoader.php';
+       }
+
+       function execute() {
+               $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
+               $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
+               $this->parserTest = new ParserTest;
+               $this->fuzzTest( $files );
+       }
+
+       /**
+        * Run a fuzz test series
+        * Draw input from a set of test files
+        * @param array $filenames
+        */
+       function fuzzTest( $filenames ) {
+               $GLOBALS['wgContLang'] = Language::factory( 'en' );
+               $dict = $this->getFuzzInput( $filenames );
+               $dictSize = strlen( $dict );
+               $logMaxLength = log( $this->maxFuzzTestLength );
+               $this->parserTest->setupDatabase();
+               ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
+
+               $numTotal = 0;
+               $numSuccess = 0;
+               $user = new User;
+               $opts = ParserOptions::newFromUser( $user );
+               $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+               while ( true ) {
+                       // Generate test input
+                       mt_srand( ++$this->seed );
+                       $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+                       $input = '';
+
+                       while ( strlen( $input ) < $totalLength ) {
+                               $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+                               $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+                               $offset = mt_rand( 0, $dictSize - $hairLength );
+                               $input .= substr( $dict, $offset, $hairLength );
+                       }
+
+                       $this->parserTest->setupGlobals();
+                       $parser = $this->parserTest->getParser();
+
+                       // Run the test
+                       try {
+                               $parser->parse( $input, $title, $opts );
+                               $fail = false;
+                       } catch ( Exception $exception ) {
+                               $fail = true;
+                       }
+
+                       if ( $fail ) {
+                               echo "Test failed with seed {$this->seed}\n";
+                               echo "Input:\n";
+                               printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
+                               echo "$exception\n";
+                       } else {
+                               $numSuccess++;
+                       }
+
+                       $numTotal++;
+                       $this->parserTest->teardownGlobals();
+                       $parser->__destruct();
+
+                       if ( $numTotal % 100 == 0 ) {
+                               $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+                               echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
+                               if ( $usage >= 100 ) {
+                                       echo "Out of memory:\n";
+                                       $memStats = $this->getMemoryBreakdown();
+
+                                       foreach ( $memStats as $name => $usage ) {
+                                               echo "$name: $usage\n";
+                                       }
+                                       if ( function_exists( 'hphpd_break' ) ) {
+                                               hphpd_break();
+                                       }
+                                       return;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Get a memory usage breakdown
+        * @return array
+        */
+       function getMemoryBreakdown() {
+               $memStats = [];
+
+               foreach ( $GLOBALS as $name => $value ) {
+                       $memStats['$' . $name] = $this->guessVarSize( $value );
+               }
+
+               $classes = get_declared_classes();
+
+               foreach ( $classes as $class ) {
+                       $rc = new ReflectionClass( $class );
+                       $props = $rc->getStaticProperties();
+                       $memStats[$class] = $this->guessVarSize( $props );
+                       $methods = $rc->getMethods();
+
+                       foreach ( $methods as $method ) {
+                               $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
+                       }
+               }
+
+               $functions = get_defined_functions();
+
+               foreach ( $functions['user'] as $function ) {
+                       $rf = new ReflectionFunction( $function );
+                       $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
+               }
+
+               asort( $memStats );
+
+               return $memStats;
+       }
+
+       /**
+        * Estimate the size of the input variable
+        */
+       function guessVarSize( $var ) {
+               $length = 0;
+               try {
+                       MediaWiki\suppressWarnings();
+                       $length = strlen( serialize( $var ) );
+                       MediaWiki\restoreWarnings();
+               } catch ( Exception $e ) {
+               }
+               return $length;
+       }
+
+       /**
+        * Get an input dictionary from a set of parser test files
+        * @param array $filenames
+        * @return string
+        */
+       function getFuzzInput( $filenames ) {
+               $dict = '';
+
+               foreach ( $filenames as $filename ) {
+                       $contents = file_get_contents( $filename );
+                       preg_match_all(
+                               '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
+                               $contents,
+                               $matches
+                       );
+
+                       foreach ( $matches[1] as $match ) {
+                               $dict .= $match . "\n";
+                       }
+               }
+
+               return $dict;
+       }
+}
+
+$maintClass = 'ParserFuzzTest';
+require RUN_MAINTENANCE_IF_MAIN;
diff --git a/tests/parser/parserTest.inc b/tests/parser/parserTest.inc
deleted file mode 100644 (file)
index e965e2d..0000000
+++ /dev/null
@@ -1,1815 +0,0 @@
-<?php
-/**
- * Helper code for the MediaWiki parser test suite. Some code is duplicated
- * in PHPUnit's NewParserTests.php, so you'll probably want to update both
- * at the same time.
- *
- * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com>
- * 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 $maxFuzzTestLength = 300;
-       private $fuzzSeed = 0;
-       private $memoryLimit = 50;
-       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";
-               }
-
-               if ( isset( $options['seed'] ) ) {
-                       $this->fuzzSeed = intval( $options['seed'] ) - 1;
-               }
-
-               $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 fuzz test series
-        * Draw input from a set of test files
-        * @param array $filenames
-        */
-       function fuzzTest( $filenames ) {
-               $GLOBALS['wgContLang'] = Language::factory( 'en' );
-               $dict = $this->getFuzzInput( $filenames );
-               $dictSize = strlen( $dict );
-               $logMaxLength = log( $this->maxFuzzTestLength );
-               $this->setupDatabase();
-               ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
-
-               $numTotal = 0;
-               $numSuccess = 0;
-               $user = new User;
-               $opts = ParserOptions::newFromUser( $user );
-               $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
-
-               while ( true ) {
-                       // Generate test input
-                       mt_srand( ++$this->fuzzSeed );
-                       $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
-                       $input = '';
-
-                       while ( strlen( $input ) < $totalLength ) {
-                               $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
-                               $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
-                               $offset = mt_rand( 0, $dictSize - $hairLength );
-                               $input .= substr( $dict, $offset, $hairLength );
-                       }
-
-                       $this->setupGlobals();
-                       $parser = $this->getParser();
-
-                       // Run the test
-                       try {
-                               $parser->parse( $input, $title, $opts );
-                               $fail = false;
-                       } catch ( Exception $exception ) {
-                               $fail = true;
-                       }
-
-                       if ( $fail ) {
-                               echo "Test failed with seed {$this->fuzzSeed}\n";
-                               echo "Input:\n";
-                               printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
-                               echo "$exception\n";
-                       } else {
-                               $numSuccess++;
-                       }
-
-                       $numTotal++;
-                       $this->teardownGlobals();
-                       $parser->__destruct();
-
-                       if ( $numTotal % 100 == 0 ) {
-                               $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
-                               echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
-                               if ( $usage > 90 ) {
-                                       echo "Out of memory:\n";
-                                       $memStats = $this->getMemoryBreakdown();
-
-                                       foreach ( $memStats as $name => $usage ) {
-                                               echo "$name: $usage\n";
-                                       }
-                                       $this->abort();
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Get an input dictionary from a set of parser test files
-        * @param array $filenames
-        * @return string
-        */
-       function getFuzzInput( $filenames ) {
-               $dict = '';
-
-               foreach ( $filenames as $filename ) {
-                       $contents = file_get_contents( $filename );
-                       preg_match_all(
-                               '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
-                               $contents,
-                               $matches
-                       );
-
-                       foreach ( $matches[1] as $match ) {
-                               $dict .= $match . "\n";
-                       }
-               }
-
-               return $dict;
-       }
-
-       /**
-        * Get a memory usage breakdown
-        * @return array
-        */
-       function getMemoryBreakdown() {
-               $memStats = [];
-
-               foreach ( $GLOBALS as $name => $value ) {
-                       $memStats['$' . $name] = strlen( serialize( $value ) );
-               }
-
-               $classes = get_declared_classes();
-
-               foreach ( $classes as $class ) {
-                       $rc = new ReflectionClass( $class );
-                       $props = $rc->getStaticProperties();
-                       $memStats[$class] = strlen( serialize( $props ) );
-                       $methods = $rc->getMethods();
-
-                       foreach ( $methods as $method ) {
-                               $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
-                       }
-               }
-
-               $functions = get_defined_functions();
-
-               foreach ( $functions['user'] as $function ) {
-                       $rf = new ReflectionFunction( $function );
-                       $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
-               }
-
-               asort( $memStats );
-
-               return $memStats;
-       }
-
-       function abort() {
-               $this->abort();
-       }
-
-       /**
-        * 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)
-                       (?<qstr>                                        # Quoted string
-                               "
-                               (?:[^\\\\"] | \\\\.)*
-                               "
-                       )
-                       (?<json>
-                               \{              # Open bracket
-                               (?:
-                                       [^"{}] |                                # Not a quoted string or object, or
-                                       (?&qstr) |                              # A quoted string, or
-                                       (?&json)                                # A json object (recursively)
-                               )*
-                               \}              # Close bracket
-                       )
-                       (?<value>
-                               (?:
-                                       (?&qstr)                        # Quoted val
-                               |
-                                       \[\[
-                                               [^]]*                   # Link target
-                                       \]\]
-                               |
-                                       [\w-]+                          # Plain word
-                               |
-                                       (?&json)                        # JSON object
-                               )
-                       )
-               )';
-               $regex = '/' . $defs . '\b
-                       (?<k>[\w-]+)                            # Key
-                       \b
-                       (?:\s*
-                               =                                               # First sub-value
-                               \s*
-                               (?<v>
-                                       (?&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
-        */
-       private 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' => '<?xml version="1.0" ?>
-<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
-<DjVuXML>
-<HEAD></HEAD>
-<BODY><OBJECT height="3508" width="2480">
-<PARAM name="DPI" value="300" />
-<PARAM name="GAMMA" value="2.2" />
-</OBJECT>
-<OBJECT height="3508" width="2480">
-<PARAM name="DPI" value="300" />
-<PARAM name="GAMMA" value="2.2" />
-</OBJECT>
-<OBJECT height="3508" width="2480">
-<PARAM name="DPI" value="300" />
-<PARAM name="GAMMA" value="2.2" />
-</OBJECT>
-<OBJECT height="3508" width="2480">
-<PARAM name="DPI" value="300" />
-<PARAM name="GAMMA" value="2.2" />
-</OBJECT>
-<OBJECT height="3508" width="2480">
-<PARAM name="DPI" value="300" />
-<PARAM name="GAMMA" value="2.2" />
-</OBJECT>
-</BODY>
-</DjVuXML>',
-                       '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",
-                       '<?xml version="1.0" encoding="utf-8"?>' .
-                       '<svg xmlns="http://www.w3.org/2000/svg"' .
-                       ' version="1.1" width="240" height="180"/>' );
-               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.
-        */
-       private 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() .
-                               '<html>' .
-                               $text .
-                               '</html>';
-
-               $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;
-       }
-}
-
-class ParserTestResultNormalizer {
-       protected $doc, $xpath, $invalid;
-
-       public static function normalize( $text, $funcs ) {
-               $norm = new self( $text );
-               if ( $norm->invalid ) {
-                       return $text;
-               }
-               foreach ( $funcs as $func ) {
-                       $norm->$func();
-               }
-               return $norm->serialize();
-       }
-
-       protected function __construct( $text ) {
-               $this->doc = new DOMDocument( '1.0', 'utf-8' );
-
-               // Note: parsing a supposedly XHTML document with an XML parser is not
-               // guaranteed to give accurate results. For example, it may introduce
-               // differences in the number of line breaks in <pre> tags.
-
-               MediaWiki\suppressWarnings();
-               if ( !$this->doc->loadXML( '<html><body>' . $text . '</body></html>' ) ) {
-                       $this->invalid = true;
-               }
-               MediaWiki\restoreWarnings();
-               $this->xpath = new DOMXPath( $this->doc );
-               $this->body = $this->xpath->query( '//body' )->item( 0 );
-       }
-
-       protected function removeTbody() {
-               foreach ( $this->xpath->query( '//tbody' ) as $tbody ) {
-                       while ( $tbody->firstChild ) {
-                               $child = $tbody->firstChild;
-                               $tbody->removeChild( $child );
-                               $tbody->parentNode->insertBefore( $child, $tbody );
-                       }
-                       $tbody->parentNode->removeChild( $tbody );
-               }
-       }
-
-       /**
-        * The point of this function is to produce a normalized DOM in which
-        * Tidy's output matches the output of html5depurate. Tidy both trims
-        * and pretty-prints, so this requires fairly aggressive treatment.
-        *
-        * In particular, note that Tidy converts <pre>x</pre> to <pre>\nx\n</pre>,
-        * which theoretically affects display since the second line break is not
-        * ignored by compliant HTML parsers.
-        *
-        * This function also removes empty elements, as does Tidy.
-        */
-       protected function trimWhitespace() {
-               foreach ( $this->xpath->query( '//text()' ) as $child ) {
-                       if ( strtolower( $child->parentNode->nodeName ) === 'pre' ) {
-                               // Just trim one line break from the start and end
-                               if ( substr_compare( $child->data, "\n", 0 ) === 0 ) {
-                                       $child->data = substr( $child->data, 1 );
-                               }
-                               if ( substr_compare( $child->data, "\n", -1 ) === 0 ) {
-                                       $child->data = substr( $child->data, 0, -1 );
-                               }
-                       } else {
-                               // Trim all whitespace
-                               $child->data = trim( $child->data );
-                       }
-                       if ( $child->data === '' ) {
-                               $child->parentNode->removeChild( $child );
-                       }
-               }
-       }
-
-       /**
-        * Serialize the XML DOM for comparison purposes. This does not generate HTML.
-        */
-       protected function serialize() {
-               return strtr( $this->doc->saveXML( $this->body ),
-                       [ '<body>' => '', '</body>' => '' ] );
-       }
-}
diff --git a/tests/parser/parserTestsParserHook.php b/tests/parser/parserTestsParserHook.php
deleted file mode 100644 (file)
index 5bf50ea..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/**
- * A basic extension that's used by the parser tests to test whether input and
- * arguments are passed to extensions properly.
- *
- * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
- *
- * 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
- * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
- */
-
-class ParserTestParserHook {
-
-       static function setup( &$parser ) {
-               $parser->setHook( 'tag', [ __CLASS__, 'dumpHook' ] );
-               $parser->setHook( 'tåg', [ __CLASS__, 'dumpHook' ] );
-               $parser->setHook( 'statictag', [ __CLASS__, 'staticTagHook' ] );
-               return true;
-       }
-
-       static function dumpHook( $in, $argv ) {
-               return "<pre>\n" .
-                       var_export( $in, true ) . "\n" .
-                       var_export( $argv, true ) . "\n" .
-                       "</pre>";
-       }
-
-       static function staticTagHook( $in, $argv, $parser ) {
-               if ( !count( $argv ) ) {
-                       $parser->static_tag_buf = $in;
-                       return '';
-               } elseif ( count( $argv ) === 1 && isset( $argv['action'] )
-                       && $argv['action'] === 'flush' && $in === null
-               ) {
-                       // Clear the buffer, we probably don't need to
-                       if ( isset( $parser->static_tag_buf ) ) {
-                               $tmp = $parser->static_tag_buf;
-                       } else {
-                               $tmp = '';
-                       }
-                       $parser->static_tag_buf = null;
-                       return $tmp;
-               } else { // wtf?
-                       return
-                               "\nCall this extension as <statictag>string</statictag> or as" .
-                               " <statictag action=flush/>, not in any other way.\n" .
-                               "text: " . var_export( $in, true ) . "\n" .
-                               "argv: " . var_export( $argv, true ) . "\n";
-               }
-       }
-}
index f961dd4..915eac6 100644 (file)
@@ -52,8 +52,6 @@ Options:
   --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
-  --fuzz           Do a fuzz test instead of a normal test
-  --seed <n>       Start the fuzz test from the specified seed
   --run-disabled   run disabled tests
   --run-parsoid    run parsoid tests (normally disabled)
   --dwdiff         Use dwdiff to display diff output
@@ -94,9 +92,5 @@ if ( isset( $options['file'] ) ) {
 $version = SpecialVersion::getVersion( 'nodb' );
 echo "This is MediaWiki version {$version}.\n\n";
 
-if ( isset( $options['fuzz'] ) ) {
-       $tester->fuzzTest( $files );
-} else {
-       $ok = $tester->runTestsFromFiles( $files );
-       exit( $ok ? 0 : 1 );
-}
+$ok = $tester->runTestsFromFiles( $files );
+exit( $ok ? 0 : 1 );
index 541ac11..50b3390 100644 (file)
@@ -477,6 +477,10 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        while ( $this->db->trxLevel() > 0 ) {
                                $this->db->rollback( __METHOD__, 'flush' );
                        }
+                       // Check for unsafe queries
+                       if ( $this->db->getType() === 'mysql' ) {
+                               $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'" );
+                       }
                }
 
                DeferredUpdates::clearPendingUpdates();
@@ -495,7 +499,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        }
 
        protected function tearDown() {
-               global $wgRequest;
+               global $wgRequest, $wgSQLMode;
 
                $status = ob_get_status();
                if ( isset( $status['name'] ) &&
@@ -519,6 +523,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        while ( $this->db->trxLevel() > 0 ) {
                                $this->db->rollback( __METHOD__, 'flush' );
                        }
+                       if ( $this->db->getType() === 'mysql' ) {
+                               $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ) );
+                       }
                }
 
                // Restore mw globals
index 4622a38..a5d67de 100644 (file)
@@ -1,8 +1,143 @@
 <?php
 
-class FauxRequestTest extends MediaWikiTestCase {
+class FauxRequestTest extends PHPUnit_Framework_TestCase {
+       /**
+        * @covers FauxRequest::__construct
+        */
+       public function testConstructInvalidData() {
+               $this->setExpectedException( MWException::class, 'bogus data' );
+               $req = new FauxRequest( 'x' );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        */
+       public function testConstructInvalidSession() {
+               $this->setExpectedException( MWException::class, 'bogus session' );
+               $req = new FauxRequest( [], false, 'x' );
+       }
+
+       /**
+        * @covers FauxRequest::getText
+        */
+       public function testGetText() {
+               $req = new FauxRequest( [ 'x' => 'Value' ] );
+               $this->assertEquals( 'Value', $req->getText( 'x' ) );
+               $this->assertEquals( '', $req->getText( 'z' ) );
+       }
+
+       /**
+        * @covers FauxRequest::getValues
+        */
+       public function testGetValues() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+               $req = new FauxRequest( $values );
+               $this->assertEquals( $values, $req->getValues() );
+       }
+
+       /**
+        * @covers FauxRequest::getQueryValues
+        */
+       public function testGetQueryValues() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+
+               $req = new FauxRequest( $values );
+               $this->assertEquals( $values, $req->getQueryValues() );
+               $req = new FauxRequest( $values, /*wasPosted*/ true );
+               $this->assertEquals( [], $req->getQueryValues() );
+       }
+
+       /**
+        * @covers FauxRequest::getMethod
+        */
+       public function testGetMethod() {
+               $req = new FauxRequest( [] );
+               $this->assertEquals( 'GET', $req->getMethod() );
+               $req = new FauxRequest( [], /*wasPosted*/ true );
+               $this->assertEquals( 'POST', $req->getMethod() );
+       }
+
+       /**
+        * @covers FauxRequest::wasPosted
+        */
+       public function testWasPosted() {
+               $req = new FauxRequest( [] );
+               $this->assertFalse( $req->wasPosted() );
+               $req = new FauxRequest( [], /*wasPosted*/ true );
+               $this->assertTrue( $req->wasPosted() );
+       }
+
+       /**
+        * @covers FauxRequest::getCookie
+        * @covers FauxRequest::setCookie
+        * @covers FauxRequest::setCookies
+        */
+       public function testCookies() {
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getCookie( 'z', '' ) );
+
+               $req->setCookie( 'x', 'Value', '' );
+               $this->assertEquals( 'Value', $req->getCookie( 'x', '' ) );
+
+               $req->setCookies( [ 'x' => 'One', 'y' => 'Two' ], '' );
+               $this->assertEquals( 'One', $req->getCookie( 'x', '' ) );
+               $this->assertEquals( 'Two', $req->getCookie( 'y', '' ) );
+       }
+
+       /**
+        * @covers FauxRequest::getCookie
+        * @covers FauxRequest::setCookie
+        * @covers FauxRequest::setCookies
+        */
+       public function testCookiesDefaultPrefix() {
+               global $wgCookiePrefix;
+               $oldPrefix = $wgCookiePrefix;
+               $wgCookiePrefix = '_';
+
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getCookie( 'z' ) );
+
+               $req->setCookie( 'x', 'Value' );
+               $this->assertEquals( 'Value', $req->getCookie( 'x' ) );
+
+               $wgCookiePrefix = $oldPrefix;
+       }
+
+       /**
+        * @covers FauxRequest::getRequestURL
+        */
+       public function testGetRequestURL() {
+               $req = new FauxRequest();
+               $this->setExpectedException( MWException::class );
+               $req->getRequestURL();
+       }
+
+       /**
+        * @covers FauxRequest::setRequestURL
+        * @covers FauxRequest::getRequestURL
+        */
+       public function testSetRequestURL() {
+               $req = new FauxRequest();
+               $req->setRequestURL( 'https://example.org' );
+               $this->assertEquals( 'https://example.org', $req->getRequestURL() );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        * @covers FauxRequest::getProtocol
+        */
+       public function testProtocol() {
+               $req = new FauxRequest();
+               $this->assertEquals( 'http', $req->getProtocol() );
+               $req = new FauxRequest( [], false, null, 'http' );
+               $this->assertEquals( 'http', $req->getProtocol() );
+               $req = new FauxRequest( [], false, null, 'https' );
+               $this->assertEquals( 'https', $req->getProtocol() );
+       }
+
        /**
         * @covers FauxRequest::setHeader
+        * @covers FauxRequest::setHeaders
         * @covers FauxRequest::getHeader
         */
        public function testGetSetHeader() {
@@ -22,7 +157,7 @@ class FauxRequestTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers FauxRequest::getAllHeaders
+        * @covers FauxRequest::initHeaders
         */
        public function testGetAllHeaders() {
                $_SERVER['HTTP_TEST'] = 'Example';
@@ -33,19 +168,36 @@ class FauxRequestTest extends MediaWikiTestCase {
                        [],
                        $request->getAllHeaders()
                );
+
+               $this->assertEquals(
+                       false,
+                       $request->getHeader( 'test' )
+               );
        }
 
        /**
-        * @covers FauxRequest::getHeader
+        * @covers FauxRequest::__construct
+        * @covers FauxRequest::getSessionArray
         */
-       public function testGetHeader() {
-               $_SERVER['HTTP_TEST'] = 'Example';
+       public function testSessionData() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
 
-               $request = new FauxRequest();
+               $req = new FauxRequest( [], false, /*session*/ $values );
+               $this->assertEquals( $values, $req->getSessionArray() );
 
-               $this->assertEquals(
-                       false,
-                       $request->getHeader( 'test' )
-               );
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getSessionArray() );
+       }
+
+       /**
+        * @covers FauxRequest::getRawQueryString
+        * @covers FauxRequest::getRawPostString
+        * @covers FauxRequest::getRawInput
+        */
+       public function testDummies() {
+               $req = new FauxRequest();
+               $this->assertEquals( '', $req->getRawQueryString() );
+               $this->assertEquals( '', $req->getRawPostString() );
+               $this->assertEquals( '', $req->getRawInput() );
        }
 }
index c946689..0f1634b 100644 (file)
@@ -23,8 +23,11 @@ class WebRequestTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideDetectServer
         * @covers WebRequest::detectServer
+        * @covers WebRequest::detectProtocol
         */
        public function testDetectServer( $expected, $input, $description ) {
+               $this->setMwGlobals( 'wgAssumeProxiesUseDefaultProtocolPorts', true );
+
                $_SERVER = $input;
                $result = WebRequest::detectServer();
                $this->assertEquals( $expected, $result, $description );
@@ -63,6 +66,24 @@ class WebRequestTest extends MediaWikiTestCase {
                                ],
                                'Secure off'
                        ],
+                       [
+                               'https://x',
+                               [
+                                       'HTTP_HOST' => 'x',
+                                       'HTTP_X_FORWARDED_PROTO' => 'https',
+                               ],
+                               'Forwarded HTTPS'
+                       ],
+                       [
+                               'https://x',
+                               [
+                                       'HTTP_HOST' => 'x',
+                                       'HTTPS' => 'off',
+                                       'SERVER_PORT' => '81',
+                                       'HTTP_X_FORWARDED_PROTO' => 'https',
+                               ],
+                               'Forwarded HTTPS'
+                       ],
                        [
                                'http://y',
                                [
@@ -104,6 +125,217 @@ class WebRequestTest extends MediaWikiTestCase {
                ];
        }
 
+       protected function mockWebRequest( $data = [] ) {
+               // Cannot use PHPUnit getMockBuilder() as it does not support
+               // overriding protected properties afterwards
+               $reflection = new ReflectionClass( 'WebRequest' );
+               $req = $reflection->newInstanceWithoutConstructor();
+
+               $prop = $reflection->getProperty( 'data' );
+               $prop->setAccessible( true );
+               $prop->setValue( $req, $data );
+
+               return $req;
+       }
+
+       /**
+        * @covers WebRequest::getElapsedTime
+        */
+       public function testGetElapsedTime() {
+               $req = new FauxRequest();
+               $this->assertGreaterThanOrEqual( 0.0, $req->getElapsedTime() );
+               $this->assertEquals( 0.0, $req->getElapsedTime(), '', /*delta*/ 0.2 );
+       }
+
+       /**
+        * @covers WebRequest::getVal
+        * @covers WebRequest::getGPCVal
+        * @covers WebRequest::normalizeUnicode
+        */
+       public function testGetValNormal() {
+               // Assert that WebRequest normalises GPC data using UtfNormal\Validator
+               $input = "a \x00 null";
+               $normal = "a \xef\xbf\xbd null";
+               $req = new FauxRequest( [ 'x' => $input, 'y' => [ $input, $input ] ] );
+               $this->assertSame( $normal, $req->getVal( 'x' ) );
+               $this->assertNotSame( $input, $req->getVal( 'x' ) );
+               $this->assertSame( [ $normal, $normal ], $req->getArray( 'y' ) );
+       }
+
+       /**
+        * @covers WebRequest::getVal
+        * @covers WebRequest::getGPCVal
+        */
+       public function testGetVal() {
+               $req = new FauxRequest( [ 'x' => 'Value', 'y' => [ 'a' ], 'crlf' => "A\r\nb" ] );
+               $this->assertSame( 'Value', $req->getVal( 'x' ), 'Simple value' );
+               $this->assertSame( null, $req->getVal( 'z' ), 'Not found' );
+               $this->assertSame( null, $req->getVal( 'y' ), 'Array is ignored' );
+               $this->assertSame( "A\r\nb", $req->getVal( 'crlf' ), 'CRLF' );
+       }
+
+       /**
+        * @covers WebRequest::getRawVal
+        */
+       public function testGetRawVal() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'crlf' => "A\r\nb"
+               ] );
+               $this->assertSame( 'Value', $req->getRawVal( 'x' ) );
+               $this->assertSame( null, $req->getRawVal( 'z' ), 'Not found' );
+               $this->assertSame( null, $req->getRawVal( 'y' ), 'Array is ignored' );
+               $this->assertSame( "A\r\nb", $req->getRawVal( 'crlf' ), 'CRLF' );
+       }
+
+       /**
+        * @covers WebRequest::getArray
+        */
+       public function testGetArray() {
+               $req = new FauxRequest( [ 'x' => 'Value', 'y' => [ 'a', 'b' ] ] );
+               $this->assertSame( [ 'Value' ], $req->getArray( 'x' ), 'Value becomes array' );
+               $this->assertSame( null, $req->getArray( 'z' ), 'Not found' );
+               $this->assertSame( [ 'a', 'b' ], $req->getArray( 'y' ) );
+       }
+
+       /**
+        * @covers WebRequest::getIntArray
+        */
+       public function testGetIntArray() {
+               $req = new FauxRequest( [ 'x' => [ 'Value' ], 'y' => [ '0', '4.2', '-2' ] ] );
+               $this->assertSame( [ 0 ], $req->getIntArray( 'x' ), 'Text becomes 0' );
+               $this->assertSame( null, $req->getIntArray( 'z' ), 'Not found' );
+               $this->assertSame( [ 0, 4, -2 ], $req->getIntArray( 'y' ) );
+       }
+
+       /**
+        * @covers WebRequest::getInt
+        */
+       public function testGetInt() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'zero' => '0',
+                       'answer' => '4.2',
+                       'neg' => '-2',
+               ] );
+               $this->assertSame( 0, $req->getInt( 'x' ), 'Text' );
+               $this->assertSame( 0, $req->getInt( 'y' ), 'Array' );
+               $this->assertSame( 0, $req->getInt( 'z' ), 'Not found' );
+               $this->assertSame( 0, $req->getInt( 'zero' ) );
+               $this->assertSame( 4, $req->getInt( 'answer' ) );
+               $this->assertSame( -2, $req->getInt( 'neg' ) );
+       }
+
+       /**
+        * @covers WebRequest::getIntOrNull
+        */
+       public function testGetIntOrNull() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'zero' => '0',
+                       'answer' => '4.2',
+                       'neg' => '-2',
+               ] );
+               $this->assertSame( null, $req->getIntOrNull( 'x' ), 'Text' );
+               $this->assertSame( null, $req->getIntOrNull( 'y' ), 'Array' );
+               $this->assertSame( null, $req->getIntOrNull( 'z' ), 'Not found' );
+               $this->assertSame( 0, $req->getIntOrNull( 'zero' ) );
+               $this->assertSame( 4, $req->getIntOrNull( 'answer' ) );
+               $this->assertSame( -2, $req->getIntOrNull( 'neg' ) );
+       }
+
+       /**
+        * @covers WebRequest::getFloat
+        */
+       public function testGetFloat() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'zero' => '0',
+                       'answer' => '4.2',
+                       'neg' => '-2',
+               ] );
+               $this->assertSame( 0.0, $req->getFloat( 'x' ), 'Text' );
+               $this->assertSame( 0.0, $req->getFloat( 'y' ), 'Array' );
+               $this->assertSame( 0.0, $req->getFloat( 'z' ), 'Not found' );
+               $this->assertSame( 0.0, $req->getFloat( 'zero' ) );
+               $this->assertSame( 4.2, $req->getFloat( 'answer' ) );
+               $this->assertSame( -2.0, $req->getFloat( 'neg' ) );
+       }
+
+       /**
+        * @covers WebRequest::getBool
+        */
+       public function testGetBool() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'zero' => '0',
+                       'f' => 'false',
+                       't' => 'true',
+               ] );
+               $this->assertSame( true, $req->getBool( 'x' ), 'Text' );
+               $this->assertSame( false, $req->getBool( 'y' ), 'Array' );
+               $this->assertSame( false, $req->getBool( 'z' ), 'Not found' );
+               $this->assertSame( false, $req->getBool( 'zero' ) );
+               $this->assertSame( true, $req->getBool( 'f' ) );
+               $this->assertSame( true, $req->getBool( 't' ) );
+       }
+
+       /**
+        * @covers WebRequest::getFuzzyBool
+        */
+       public function testGetFuzzyBool() {
+               $req = new FauxRequest( [ 'x' => 'Value', 'f' => 'false', 't' => 'true' ] );
+               $this->assertSame( true, $req->getFuzzyBool( 'x' ), 'Text' );
+               $this->assertSame( false, $req->getFuzzyBool( 'z' ), 'Not found' );
+               $this->assertSame( false, $req->getFuzzyBool( 'f' ) );
+               $this->assertSame( true, $req->getFuzzyBool( 't' ) );
+       }
+
+       /**
+        * @covers WebRequest::getCheck
+        */
+       public function testGetCheck() {
+               $req = new FauxRequest( [ 'x' => 'Value', 'zero' => '0' ] );
+               $this->assertSame( false, $req->getCheck( 'z' ), 'Not found' );
+               $this->assertSame( true, $req->getCheck( 'x' ), 'Text' );
+               $this->assertSame( true, $req->getCheck( 'zero' ) );
+       }
+
+       /**
+        * @covers WebRequest::getText
+        */
+       public function testGetText() {
+               // FauxRequest overrides getText
+               $req = $this->mockWebRequest( [ 'crlf' => "Va\r\nlue" ] );
+               $this->assertSame( "Va\nlue", $req->getText( 'crlf' ), 'CR stripped' );
+       }
+
+       /**
+        * @covers WebRequest::getValues
+        */
+       public function testGetValues() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+               // FauxRequest overrides getValues
+               $req = $this->mockWebRequest( $values );
+               $this->assertSame( $values, $req->getValues() );
+               $this->assertSame( [ 'x' => 'Value' ], $req->getValues( 'x' ), 'Specific keys' );
+       }
+
+       /**
+        * @covers WebRequest::getValueNames
+        */
+       public function testGetValueNames() {
+               // FauxRequest overrides getValues
+               $req = new FauxRequest( [ 'x' => 'Value', 'y' => '' ] );
+               $this->assertSame( [ 'x', 'y' ], $req->getValueNames() );
+               $this->assertSame( [ 'x' ], $req->getValueNames( [ 'y' ] ), 'Exclude keys' );
+       }
+
        /**
         * @dataProvider provideGetIP
         * @covers WebRequest::getIP
@@ -343,6 +575,7 @@ class WebRequestTest extends MediaWikiTestCase {
                                [ 'en-gb' => 1, 'en-us' => '1' ],
                                'Two equally prefered English variants'
                        ],
+                       [ '_', [], 'Invalid input' ],
                ];
        }
 
index 7a8e208..02d0a0d 100644 (file)
@@ -513,4 +513,57 @@ class ApiEditPageTest extends ApiTestCase {
                $this->assertEquals( "testing-nontext", $page->getContentModel() );
                $this->assertEquals( $data, $page->getContent()->serialize() );
        }
+
+       /**
+        * This test verifies that after changing the content model
+        * of a page, undoing that edit via the API will also
+        * undo the content model change.
+        */
+       public function testUndoAfterContentModelChange() {
+               $name = 'Help:' . __FUNCTION__;
+               $uploader = self::$users['uploader']->getUser();
+               $sysop = self::$users['sysop']->getUser();
+               $apiResult = $this->doApiRequestWithToken( [
+                       'action' => 'edit',
+                       'title' => $name,
+                       'text' => 'some text',
+               ], null, $sysop )[0];
+
+               // Check success
+               $this->assertArrayHasKey( 'edit', $apiResult );
+               $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+               $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+               $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+               // Content model is wikitext
+               $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] );
+
+               // Convert the page to JSON
+               $apiResult = $this->doApiRequestWithToken( [
+                       'action' => 'edit',
+                       'title' => $name,
+                       'text' => '{}',
+                       'contentmodel' => 'json',
+               ], null, $uploader )[0];
+
+               // Check success
+               $this->assertArrayHasKey( 'edit', $apiResult );
+               $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+               $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+               $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+               $this->assertEquals( 'json', $apiResult['edit']['contentmodel'] );
+
+               $apiResult = $this->doApiRequestWithToken( [
+                       'action' => 'edit',
+                       'title' => $name,
+                       'undo' => $apiResult['edit']['newrevid']
+               ], null, $sysop )[0];
+
+               // Check success
+               $this->assertArrayHasKey( 'edit', $apiResult );
+               $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+               $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+               $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+               // Check that the contentmodel is back to wikitext now.
+               $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] );
+       }
 }
index 788d304..f679f63 100644 (file)
@@ -2709,9 +2709,11 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $session->clear();
                $user = $this->getMock( 'User', [ 'addToDatabase' ] );
                $user->expects( $this->once() )->method( 'addToDatabase' )
-                       ->will( $this->returnCallback( function () use ( $username ) {
-                               $status = \User::newFromName( $username )->addToDatabase();
+                       ->will( $this->returnCallback( function () use ( $username, &$user ) {
+                               $oldUser = \User::newFromName( $username );
+                               $status = $oldUser->addToDatabase();
                                $this->assertTrue( $status->isOK(), 'sanity check' );
+                               $user->setId( $oldUser->getId() );
                                return \Status::newFatal( 'userexists' );
                        } ) );
                $user->setName( $username );
diff --git a/tests/phpunit/includes/content/JsonContentHandlerTest.php b/tests/phpunit/includes/content/JsonContentHandlerTest.php
new file mode 100644 (file)
index 0000000..abfb673
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+class JsonContentHandlerTest extends MediaWikiTestCase {
+
+       /**
+        * @covers JsonContentHandler::makeEmptyContent
+        */
+       public function testMakeEmptyContent() {
+               $handler = new JsonContentHandler();
+               $content = $handler->makeEmptyContent();
+               $this->assertInstanceOf( JsonContent::class, $content );
+               $this->assertTrue( $content->isValid() );
+       }
+}
index 16297ad..0f9a401 100644 (file)
@@ -324,18 +324,18 @@ class DatabaseTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::clearSnapshot()
+        * @covers DatabaseBase::flushSnapshot()
         */
-       public function testClearSnapshot() {
+       public function testFlushSnapshot() {
                $db = $this->db;
 
-               $db->clearSnapshot( __METHOD__ ); // ok
-               $db->clearSnapshot( __METHOD__ ); // ok
+               $db->flushSnapshot( __METHOD__ ); // ok
+               $db->flushSnapshot( __METHOD__ ); // ok
 
                $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
                $db->query( 'SELECT 1', __METHOD__ );
                $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
-               $db->clearSnapshot( __METHOD__ ); // ok
+               $db->flushSnapshot( __METHOD__ ); // ok
                $db->restoreFlags( $db::RESTORE_PRIOR );
 
                $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
index b75adca..9ce93d6 100644 (file)
@@ -102,6 +102,31 @@ class WaitConditionLoopTest extends PHPUnit_Framework_TestCase {
                $loop->setWallClock( $wallClock );
                $this->assertEquals( $loop::CONDITION_TIMED_OUT, $loop->invoke() );
                $this->assertEquals( [ 1, 1, 1 ], [ $x, $y, $z ], "Busy work done" );
+
+               $loop = new WaitConditionLoopFakeTime(
+                       function () use ( &$count, &$wallClock ) {
+                               $wallClock += 3;
+                               ++$count;
+
+                               return true;
+                       },
+                       0.0,
+                       $this->newBusyWork( $x, $y, $z, $wallClock )
+               );
+               $this->assertEquals( $loop::CONDITION_REACHED, $loop->invoke() );
+
+               $count = 0;
+               $loop = new WaitConditionLoopFakeTime(
+                       function () use ( &$count, &$wallClock ) {
+                               $wallClock += 3;
+                               ++$count;
+
+                               return $count > 10 ? true : false;
+                       },
+                       0,
+                       $this->newBusyWork( $x, $y, $z, $wallClock )
+               );
+               $this->assertEquals( $loop::CONDITION_FAILED, $loop->invoke() );
        }
 
        public function testCallbackAborted() {
index cef31dc..99b959b 100644 (file)
@@ -161,9 +161,15 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                $cKey1 = wfRandomString();
                $cKey2 = wfRandomString();
 
+               $priorValue = null;
+               $priorAsOf = null;
                $wasSet = 0;
-               $func = function( $old, &$ttl ) use ( &$wasSet, $value ) {
+               $func = function( $old, &$ttl, &$opts, $asOf )
+                       use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
+               {
                        ++$wasSet;
+                       $priorValue = $old;
+                       $priorAsOf = $asOf;
                        $ttl = 20; // override with another value
                        return $value;
                };
@@ -172,6 +178,8 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
                $this->assertEquals( $value, $v, "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertFalse( $priorValue, "No prior value" );
+               $this->assertNull( $priorAsOf, "No prior value" );
 
                $curTTL = null;
                $cache->get( $key, $curTTL );
@@ -194,6 +202,8 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                );
                $this->assertEquals( $value, $v, "Value returned" );
                $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+               $this->assertEquals( $value, $priorValue, "Has prior value" );
+               $this->assertType( 'float', $priorAsOf, "Has prior value" );
                $t1 = $cache->getCheckKeyTime( $cKey1 );
                $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
                $t2 = $cache->getCheckKeyTime( $cKey2 );
index ad84c20..097e413 100644 (file)
@@ -29,11 +29,6 @@ class NewParserTest extends MediaWikiTestCase {
        public $functionHooks = [];
        public $transparentHooks = [];
 
-       // Fuzz test
-       public $maxFuzzTestLength = 300;
-       public $fuzzSeed = 0;
-       public $memoryLimit = 50;
-
        /**
         * @var DjVuSupport
         */
@@ -837,144 +832,6 @@ class NewParserTest extends MediaWikiTestCase {
                $this->assertEquals( $result, $out, $desc );
        }
 
-       /**
-        * Run a fuzz test series
-        * Draw input from a set of test files
-        *
-        * @todo fixme Needs some work to not eat memory until the world explodes
-        *
-        * @group ParserFuzz
-        */
-       public function testFuzzTests() {
-               global $wgParserTestFiles;
-
-               $files = $wgParserTestFiles;
-
-               if ( $this->getCliArg( 'file' ) ) {
-                       $files = [ $this->getCliArg( 'file' ) ];
-               }
-
-               $dict = $this->getFuzzInput( $files );
-               $dictSize = strlen( $dict );
-               $logMaxLength = log( $this->maxFuzzTestLength );
-
-               ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
-
-               $user = new User;
-               $opts = ParserOptions::newFromUser( $user );
-               $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
-
-               $id = 1;
-
-               while ( true ) {
-
-                       // Generate test input
-                       mt_srand( ++$this->fuzzSeed );
-                       $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
-                       $input = '';
-
-                       while ( strlen( $input ) < $totalLength ) {
-                               $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
-                               $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
-                               $offset = mt_rand( 0, $dictSize - $hairLength );
-                               $input .= substr( $dict, $offset, $hairLength );
-                       }
-
-                       $this->setupGlobals();
-                       $parser = $this->getParser();
-
-                       // Run the test
-                       try {
-                               $parser->parse( $input, $title, $opts );
-                               $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
-                       } catch ( Exception $exception ) {
-                               $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
-
-                               $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\n" .
-                                       "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" .
-                                       "Backtrace: {$exception->getTraceAsString()}" );
-                       }
-
-                       $this->teardownGlobals();
-                       $parser->__destruct();
-
-                       if ( $id % 100 == 0 ) {
-                               $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
-                               // echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
-                               if ( $usage > 90 ) {
-                                       $ret = "Out of memory:\n";
-                                       $memStats = $this->getMemoryBreakdown();
-
-                                       foreach ( $memStats as $name => $usage ) {
-                                               $ret .= "$name: $usage\n";
-                                       }
-
-                                       throw new MWException( $ret );
-                               }
-                       }
-
-                       $id++;
-               }
-       }
-
-       // Various getter functions
-
-       /**
-        * Get an input dictionary from a set of parser test files
-        * @param array $filenames
-        * @return string
-        */
-       function getFuzzInput( $filenames ) {
-               $dict = '';
-
-               foreach ( $filenames as $filename ) {
-                       $contents = file_get_contents( $filename );
-                       preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
-
-                       foreach ( $matches[1] as $match ) {
-                               $dict .= $match . "\n";
-                       }
-               }
-
-               return $dict;
-       }
-
-       /**
-        * Get a memory usage breakdown
-        * @return array
-        */
-       function getMemoryBreakdown() {
-               $memStats = [];
-
-               foreach ( $GLOBALS as $name => $value ) {
-                       $memStats['$' . $name] = strlen( serialize( $value ) );
-               }
-
-               $classes = get_declared_classes();
-
-               foreach ( $classes as $class ) {
-                       $rc = new ReflectionClass( $class );
-                       $props = $rc->getStaticProperties();
-                       $memStats[$class] = strlen( serialize( $props ) );
-                       $methods = $rc->getMethods();
-
-                       foreach ( $methods as $method ) {
-                               $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
-                       }
-               }
-
-               $functions = get_defined_functions();
-
-               foreach ( $functions['user'] as $function ) {
-                       $rf = new ReflectionFunction( $function );
-                       $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
-               }
-
-               asort( $memStats );
-
-               return $memStats;
-       }
-
        /**
         * Get a Parser object
         * @param Preprocessor $preprocessor
index 85834d7..404fd97 100644 (file)
@@ -114,25 +114,25 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
                        [ [], 'test1', true ],
                        // 'site' module with a non-empty page
                        [
-                               [ 'MediaWiki:Common.js' => [ 'rev_sha1' => 'dmh6qn', 'rev_len' => 1234 ] ],
+                               [ 'MediaWiki:Common.js' => [ 'page_len' => 1234 ] ],
                                'site',
                                false,
                        ],
                        // 'site' module with an empty page
                        [
-                               [ 'MediaWiki:Foo.js' => [ 'rev_sha1' => 'phoi', 'rev_len' => 0 ] ],
+                               [ 'MediaWiki:Foo.js' => [ 'page_len' => 0 ] ],
                                'site',
                                false,
                        ],
                        // 'user' module with a non-empty page
                        [
-                               [ 'User:Example/common.js' => [ 'rev_sha1' => 'j7ssba', 'rev_len' => 25 ] ],
+                               [ 'User:Example/common.js' => [ 'page_len' => 25 ] ],
                                'user',
                                false,
                        ],
                        // 'user' module with an empty page
                        [
-                               [ 'User:Example/foo.js' => [ 'rev_sha1' => 'phoi', 'rev_len' => 0 ] ],
+                               [ 'User:Example/foo.js' => [ 'page_len' => 0 ] ],
                                'user',
                                true,
                        ],
index a70946a..4158863 100755 (executable)
@@ -10,8 +10,6 @@
 // through this entry point or not.
 define( 'MW_PHPUNIT_TEST', true );
 
-$wgPhpUnitClass = 'PHPUnit_TextUI_Command';
-
 // Start up MediaWiki in command-line mode
 require_once dirname( dirname( __DIR__ ) ) . "/maintenance/Maintenance.php";
 
@@ -27,6 +25,7 @@ class PHPUnitMaintClass extends Maintenance {
                'use-normal-tables' => false,
                'reuse-db' => false,
                'wiki' => false,
+               'profiler' => false,
        ];
 
        public function __construct() {
@@ -167,7 +166,7 @@ class PHPUnitMaintClass extends Maintenance {
 
                // TODO: we should call MediaWikiTestCase::prepareServices( new GlobalVarConfig() ) here.
                // But PHPUnit may not be loaded yet, so we have to wait until just
-               // before PHPUnit_TextUI_Command::main() is executed at the end of this file.
+               // before PHPUnit_TextUI_Command::main() is executed.
        }
 
        public function execute() {
@@ -188,9 +187,10 @@ class PHPUnitMaintClass extends Maintenance {
                                [ '--configuration', $IP . '/tests/phpunit/suite.xml' ] );
                }
 
+               $phpUnitClass = 'PHPUnit_TextUI_Command';
+
                if ( $this->hasOption( 'with-phpunitclass' ) ) {
-                       global $wgPhpUnitClass;
-                       $wgPhpUnitClass = $this->getOption( 'with-phpunitclass' );
+                       $phpUnitClass = $this->getOption( 'with-phpunitclass' );
 
                        # Cleanup $args array so the option and its value do not
                        # pollute PHPUnit
@@ -220,6 +220,25 @@ class PHPUnitMaintClass extends Maintenance {
                        }
                }
 
+               if ( !class_exists( 'PHPUnit_Framework_TestCase' ) ) {
+                       echo "PHPUnit not found. Please install it and other dev dependencies by
+               running `composer install` in MediaWiki root directory.\n";
+                       exit( 1 );
+               }
+               if ( !class_exists( $phpUnitClass ) ) {
+                       echo "PHPUnit entry point '" . $phpUnitClass . "' not found. Please make sure you installed
+               the containing component and check the spelling of the class name.\n";
+                       exit( 1 );
+               }
+
+               echo defined( 'HHVM_VERSION' ) ?
+                       'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
+                       'Using PHP ' . PHP_VERSION . "\n";
+
+               // Prepare global services for unit tests.
+               MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
+
+               $phpUnitClass::main();
        }
 
        public function getDbType() {
@@ -250,25 +269,3 @@ class PHPUnitMaintClass extends Maintenance {
 
 $maintClass = 'PHPUnitMaintClass';
 require RUN_MAINTENANCE_IF_MAIN;
-
-if ( !class_exists( 'PHPUnit_Framework_TestCase' ) ) {
-       echo "PHPUnit not found. Please install it and other dev dependencies by
-running `composer install` in MediaWiki root directory.\n";
-       exit( 1 );
-}
-if ( !class_exists( $wgPhpUnitClass ) ) {
-       echo "PHPUnit entry point '" . $wgPhpUnitClass . "' not found. Please make sure you installed
-the containing component and check the spelling of the class name.\n";
-       exit( 1 );
-}
-
-echo defined( 'HHVM_VERSION' ) ?
-       'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
-       'Using PHP ' . PHP_VERSION . "\n";
-
-// Prepare global services for unit tests.
-// FIXME: this should be done in the finalSetup() method,
-// but PHPUnit may not have been loaded at that point.
-MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
-
-$wgPhpUnitClass::main();
diff --git a/tests/testHelpers.inc b/tests/testHelpers.inc
deleted file mode 100644 (file)
index 1369406..0000000
+++ /dev/null
@@ -1,908 +0,0 @@
-<?php
-/**
- * Recording for passing/failing tests.
- *
- * 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
- */
-
-/**
- * Interface to record parser test results.
- *
- * The ITestRecorder is a very simple interface to record the result of
- * MediaWiki parser tests. One should call start() before running the
- * full parser tests and end() once all the tests have been finished.
- * After each test, you should use record() to keep track of your tests
- * results. Finally, report() is used to generate a summary of your
- * test run, one could dump it to the console for human consumption or
- * register the result in a database for tracking purposes.
- *
- * @since 1.22
- */
-interface ITestRecorder {
-
-       /**
-        * Called at beginning of the parser test run
-        */
-       public function start();
-
-       /**
-        * Called after each test
-        * @param string $test
-        * @param integer $subtest
-        * @param bool $result
-        */
-       public function record( $test, $subtest, $result );
-
-       /**
-        * Called before finishing the test run
-        */
-       public function report();
-
-       /**
-        * Called at the end of the parser test run
-        */
-       public function end();
-
-}
-
-class TestRecorder implements ITestRecorder {
-       public $parent;
-       public $term;
-
-       function __construct( $parent ) {
-               $this->parent = $parent;
-               $this->term = $parent->term;
-       }
-
-       function start() {
-               $this->total = 0;
-               $this->success = 0;
-       }
-
-       function record( $test, $subtest, $result ) {
-               $this->total++;
-               $this->success += ( $result ? 1 : 0 );
-       }
-
-       function end() {
-               // dummy
-       }
-
-       function report() {
-               if ( $this->total > 0 ) {
-                       $this->reportPercentage( $this->success, $this->total );
-               } else {
-                       throw new MWException( "No tests found.\n" );
-               }
-       }
-
-       function reportPercentage( $success, $total ) {
-               $ratio = wfPercent( 100 * $success / $total );
-               print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
-
-               if ( $success == $total ) {
-                       print $this->term->color( 32 ) . "ALL TESTS PASSED!";
-               } else {
-                       $failed = $total - $success;
-                       print $this->term->color( 31 ) . "$failed tests failed!";
-               }
-
-               print $this->term->reset() . "\n";
-
-               return ( $success == $total );
-       }
-}
-
-class DbTestPreviewer extends TestRecorder {
-       protected $lb; // /< Database load balancer
-       protected $db; // /< Database connection to the main DB
-       protected $curRun; // /< run ID number for the current run
-       protected $prevRun; // /< run ID number for the previous run, if any
-       protected $results; // /< Result array
-
-       /**
-        * This should be called before the table prefix is changed
-        * @param TestRecorder $parent
-        */
-       function __construct( $parent ) {
-               parent::__construct( $parent );
-
-               $this->lb = wfGetLBFactory()->newMainLB();
-               // This connection will have the wiki's table prefix, not parsertest_
-               $this->db = $this->lb->getConnection( DB_MASTER );
-       }
-
-       /**
-        * Set up result recording; insert a record for the run with the date
-        * and all that fun stuff
-        */
-       function start() {
-               parent::start();
-
-               if ( !$this->db->tableExists( 'testrun', __METHOD__ )
-                       || !$this->db->tableExists( 'testitem', __METHOD__ )
-               ) {
-                       print "WARNING> `testrun` table not found in database.\n";
-                       $this->prevRun = false;
-               } else {
-                       // We'll make comparisons against the previous run later...
-                       $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
-               }
-
-               $this->results = [];
-       }
-
-       function getName( $test, $subtest ) {
-               if ( $subtest ) {
-                       return "$test subtest #$subtest";
-               } else {
-                       return $test;
-               }
-       }
-
-       function record( $test, $subtest, $result ) {
-               parent::record( $test, $subtest, $result );
-               $this->results[ $this->getName( $test, $subtest ) ] = $result;
-       }
-
-       function report() {
-               if ( $this->prevRun ) {
-                       // f = fail, p = pass, n = nonexistent
-                       // codes show before then after
-                       $table = [
-                               'fp' => 'previously failing test(s) now PASSING! :)',
-                               'pn' => 'previously PASSING test(s) removed o_O',
-                               'np' => 'new PASSING test(s) :)',
-
-                               'pf' => 'previously passing test(s) now FAILING! :(',
-                               'fn' => 'previously FAILING test(s) removed O_o',
-                               'nf' => 'new FAILING test(s) :(',
-                               'ff' => 'still FAILING test(s) :(',
-                       ];
-
-                       $prevResults = [];
-
-                       $res = $this->db->select( 'testitem', [ 'ti_name', 'ti_success' ],
-                               [ 'ti_run' => $this->prevRun ], __METHOD__ );
-
-                       foreach ( $res as $row ) {
-                               if ( !$this->parent->regex
-                                       || preg_match( "/{$this->parent->regex}/i", $row->ti_name )
-                               ) {
-                                       $prevResults[$row->ti_name] = $row->ti_success;
-                               }
-                       }
-
-                       $combined = array_keys( $this->results + $prevResults );
-
-                       # Determine breakdown by change type
-                       $breakdown = [];
-                       foreach ( $combined as $test ) {
-                               if ( !isset( $prevResults[$test] ) ) {
-                                       $before = 'n';
-                               } elseif ( $prevResults[$test] == 1 ) {
-                                       $before = 'p';
-                               } else /* if ( $prevResults[$test] == 0 )*/ {
-                                       $before = 'f';
-                               }
-
-                               if ( !isset( $this->results[$test] ) ) {
-                                       $after = 'n';
-                               } elseif ( $this->results[$test] == 1 ) {
-                                       $after = 'p';
-                               } else /*if ( $this->results[$test] == 0 ) */ {
-                                       $after = 'f';
-                               }
-
-                               $code = $before . $after;
-
-                               if ( isset( $table[$code] ) ) {
-                                       $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
-                               }
-                       }
-
-                       # Write out results
-                       foreach ( $table as $code => $label ) {
-                               if ( !empty( $breakdown[$code] ) ) {
-                                       $count = count( $breakdown[$code] );
-                                       printf( "\n%4d %s\n", $count, $label );
-
-                                       foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
-                                               print "      * $differing_test_name  [$statusInfo]\n";
-                                       }
-                               }
-                       }
-               } else {
-                       print "No previous test runs to compare against.\n";
-               }
-
-               print "\n";
-               parent::report();
-       }
-
-       /**
-        * Returns a string giving information about when a test last had a status change.
-        * Could help to track down when regressions were introduced, as distinct from tests
-        * which have never passed (which are more change requests than regressions).
-        * @param string $testname
-        * @param string $after
-        * @return string
-        */
-       private function getTestStatusInfo( $testname, $after ) {
-               // If we're looking at a test that has just been removed, then say when it first appeared.
-               if ( $after == 'n' ) {
-                       $changedRun = $this->db->selectField( 'testitem',
-                               'MIN(ti_run)',
-                               [ 'ti_name' => $testname ],
-                               __METHOD__ );
-                       $appear = $this->db->selectRow( 'testrun',
-                               [ 'tr_date', 'tr_mw_version' ],
-                               [ 'tr_id' => $changedRun ],
-                               __METHOD__ );
-
-                       return "First recorded appearance: "
-                               . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
-                               . ", " . $appear->tr_mw_version;
-               }
-
-               // Otherwise, this test has previous recorded results.
-               // See when this test last had a different result to what we're seeing now.
-               $conds = [
-                       'ti_name' => $testname,
-                       'ti_success' => ( $after == 'f' ? "1" : "0" ) ];
-
-               if ( $this->curRun ) {
-                       $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
-               }
-
-               $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
-
-               // If no record of ever having had a different result.
-               if ( is_null( $changedRun ) ) {
-                       if ( $after == "f" ) {
-                               return "Has never passed";
-                       } else {
-                               return "Has never failed";
-                       }
-               }
-
-               // Otherwise, we're looking at a test whose status has changed.
-               // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
-               // In this situation, give as much info as we can as to when it changed status.
-               $pre = $this->db->selectRow( 'testrun',
-                       [ 'tr_date', 'tr_mw_version' ],
-                       [ 'tr_id' => $changedRun ],
-                       __METHOD__ );
-               $post = $this->db->selectRow( 'testrun',
-                       [ 'tr_date', 'tr_mw_version' ],
-                       [ "tr_id > " . $this->db->addQuotes( $changedRun ) ],
-                       __METHOD__,
-                       [ "LIMIT" => 1, "ORDER BY" => 'tr_id' ]
-               );
-
-               if ( $post ) {
-                       $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
-               } else {
-                       $postDate = 'now';
-               }
-
-               return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
-                       . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
-                       . " and $postDate";
-       }
-
-       /**
-        * Close the DB connection
-        */
-       function end() {
-               $this->lb->closeAll();
-               parent::end();
-       }
-}
-
-class DbTestRecorder extends DbTestPreviewer {
-       public $version;
-
-       /**
-        * Set up result recording; insert a record for the run with the date
-        * and all that fun stuff
-        */
-       function start() {
-               $this->db->begin( __METHOD__ );
-
-               if ( !$this->db->tableExists( 'testrun' )
-                       || !$this->db->tableExists( 'testitem' )
-               ) {
-                       print "WARNING> `testrun` table not found in database. Trying to create table.\n";
-                       $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) );
-                       echo "OK, resuming.\n";
-               }
-
-               parent::start();
-
-               $this->db->insert( 'testrun',
-                       [
-                               'tr_date' => $this->db->timestamp(),
-                               'tr_mw_version' => $this->version,
-                               'tr_php_version' => PHP_VERSION,
-                               'tr_db_version' => $this->db->getServerVersion(),
-                               'tr_uname' => php_uname()
-                       ],
-                       __METHOD__ );
-               if ( $this->db->getType() === 'postgres' ) {
-                       $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
-               } else {
-                       $this->curRun = $this->db->insertId();
-               }
-       }
-
-       /**
-        * Record an individual test item's success or failure to the db
-        *
-        * @param string $test
-        * @param bool $result
-        */
-       function record( $test, $subtest, $result ) {
-               parent::record( $test, $subtest, $result );
-
-               $this->db->insert( 'testitem',
-                       [
-                               'ti_run' => $this->curRun,
-                               'ti_name' => $this->getName( $test, $subtest ),
-                               'ti_success' => $result ? 1 : 0,
-                       ],
-                       __METHOD__ );
-       }
-
-       /**
-        * Commit transaction and clean up for result recording
-        */
-       function end() {
-               $this->db->commit( __METHOD__ );
-               parent::end();
-       }
-}
-
-class TestFileIterator implements Iterator {
-       private $file;
-       private $fh;
-       /**
-        * @var ParserTest|MediaWikiParserTest An instance of ParserTest (parserTests.php)
-        *  or MediaWikiParserTest (phpunit)
-        */
-       private $parserTest;
-       private $index = 0;
-       private $test;
-       private $section = null;
-       /** String|null: current test section being analyzed */
-       private $sectionData = [];
-       private $lineNum;
-       private $eof;
-       # Create a fake parser tests which never run anything unless
-       # asked to do so. This will avoid running hooks for a disabled test
-       private $delayedParserTest;
-       private $nextSubTest = 0;
-
-       function __construct( $file, $parserTest ) {
-               $this->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];
-       }
-}
-
-/**
- * An iterator for use as a phpunit data provider. Provides the test arguments
- * in the order expected by NewParserTest::testParserTest().
- */
-class TestFileDataProvider extends TestFileIterator {
-       function current() {
-               $test = parent::current();
-               if ( $test ) {
-                       return [
-                               $test['test'],
-                               $test['input'],
-                               $test['result'],
-                               $test['options'],
-                               $test['config'],
-                       ];
-               } else {
-                       return $test;
-               }
-       }
-}
-
-/**
- * A class to delay execution of a parser test hooks.
- */
-class DelayedParserTest {
-
-       /** Initialized on construction */
-       private $hooks;
-       private $fnHooks;
-       private $transparentHooks;
-
-       public function __construct() {
-               $this->reset();
-       }
-
-       /**
-        * Init/reset or forgot about the current delayed test.
-        * Call to this will erase any hooks function that were pending.
-        */
-       public function reset() {
-               $this->hooks = [];
-               $this->fnHooks = [];
-               $this->transparentHooks = [];
-       }
-
-       /**
-        * 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
-        * @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" );
-               }
-
-               # Trigger delayed hooks. Any failure will make us abort
-               foreach ( $this->hooks as $hook ) {
-                       $ret = $parserTest->requireHook( $hook );
-                       if ( !$ret ) {
-                               return false;
-                       }
-               }
-
-               # Trigger delayed function hooks. Any failure will make us abort
-               foreach ( $this->fnHooks as $fnHook ) {
-                       $ret = $parserTest->requireFunctionHook( $fnHook );
-                       if ( !$ret ) {
-                               return false;
-                       }
-               }
-
-               # Trigger delayed transparent hooks. Any failure will make us abort
-               foreach ( $this->transparentHooks as $hook ) {
-                       $ret = $parserTest->requireTransparentHook( $hook );
-                       if ( !$ret ) {
-                               return false;
-                       }
-               }
-
-               # Delayed execution was successful.
-               return true;
-       }
-
-       /**
-        * Similar to ParserTest object but does not run anything
-        * Use unleash() to really execute the hook
-        * @param string $hook
-        */
-       public function requireHook( $hook ) {
-               $this->hooks[] = $hook;
-       }
-
-       /**
-        * Similar to ParserTest object but does not run anything
-        * Use unleash() to really execute the hook function
-        * @param string $fnHook
-        */
-       public function requireFunctionHook( $fnHook ) {
-               $this->fnHooks[] = $fnHook;
-       }
-
-       /**
-        * Similar to ParserTest object but does not run anything
-        * Use unleash() to really execute the hook function
-        * @param string $hook
-        */
-       public function requireTransparentHook( $hook ) {
-               $this->transparentHooks[] = $hook;
-       }
-
-}
-
-/**
- * Initialize and detect the DjVu files support
- */
-class DjVuSupport {
-
-       /**
-        * Initialises DjVu tools global with default values
-        */
-       public function __construct() {
-               global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
-
-               $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
-               $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
-               $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
-               $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
-
-               if ( !in_array( 'djvu', $wgFileExtensions ) ) {
-                       $wgFileExtensions[] = 'djvu';
-               }
-       }
-
-       /**
-        * Returns true if the DjVu tools are usable
-        *
-        * @return bool
-        */
-       public function isEnabled() {
-               global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
-
-               return is_executable( $wgDjvuRenderer )
-                       && is_executable( $wgDjvuDump )
-                       && is_executable( $wgDjvuToXML )
-                       && is_executable( $wgDjvuTxt );
-       }
-}
-
-/**
- * Initialize and detect the tidy support
- */
-class TidySupport {
-       private $enabled;
-       private $config;
-
-       /**
-        * Determine if there is a usable tidy.
-        */
-       public function __construct( $useConfiguration = false ) {
-               global $IP, $wgUseTidy, $wgTidyBin, $wgTidyInternal, $wgTidyConfig,
-                       $wgTidyConf, $wgTidyOpts;
-
-               $this->enabled = true;
-               if ( $useConfiguration ) {
-                       if ( $wgTidyConfig !== null ) {
-                               $this->config = $wgTidyConfig;
-                       } elseif ( $wgUseTidy ) {
-                               $this->config = [
-                                       'tidyConfigFile' => $wgTidyConf,
-                                       'debugComment' => false,
-                                       'tidyBin' => $wgTidyBin,
-                                       'tidyCommandLine' => $wgTidyOpts
-                               ];
-                               if ( $wgTidyInternal ) {
-                                       $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP';
-                               } else {
-                                       $this->config['driver'] = 'RaggettExternal';
-                               }
-                       } else {
-                               $this->enabled = false;
-                       }
-               } else {
-                       $this->config = [
-                               'tidyConfigFile' => "$IP/includes/tidy/tidy.conf",
-                               'tidyCommandLine' => '',
-                       ];
-                       if ( extension_loaded( 'tidy' ) && class_exists( 'tidy' ) ) {
-                               $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP';
-                       } else {
-                               if ( is_executable( $wgTidyBin ) ) {
-                                       $this->config['driver'] = 'RaggettExternal';
-                                       $this->config['tidyBin'] = $wgTidyBin;
-                               } else {
-                                       $path = Installer::locateExecutableInDefaultPaths( $wgTidyBin );
-                                       if ( $path !== false ) {
-                                               $this->config['driver'] = 'RaggettExternal';
-                                               $this->config['tidyBin'] = $wgTidyBin;
-                                       } else {
-                                               $this->enabled = false;
-                                       }
-                               }
-                       }
-               }
-               if ( !$this->enabled ) {
-                       $this->config = [ 'driver' => 'disabled' ];
-               }
-       }
-
-       /**
-        * Returns true if tidy is usable
-        *
-        * @return bool
-        */
-       public function isEnabled() {
-               return $this->enabled;
-       }
-
-       public function getConfig() {
-               return $this->config;
-       }
-}