Merge "resources: Add verifiable upstream for various jquery plugins"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 21 Mar 2019 08:47:34 +0000 (08:47 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 21 Mar 2019 08:47:34 +0000 (08:47 +0000)
22 files changed:
.travis.yml
docs/export-0.10.xsd
includes/ContentSecurityPolicy.php
includes/DefaultSettings.php
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiUpload.php
includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/media/MediaTransformOutput.php
includes/parser/BlockLevelPass.php
includes/watcheditem/WatchedItemStore.php
resources/lib/ooui/oojs-ui-core.js
tests/common/TestsAutoLoader.php
tests/phpunit/PHPUnit4And6Compat.php
tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php
tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php
tests/phpunit/maintenance/DumpAsserter.php [new file with mode: 0644]
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/maintenance/backupTextPassTest.php
tests/phpunit/maintenance/backup_LogTest.php
tests/phpunit/maintenance/backup_PageTest.php
tests/phpunit/maintenance/xml.xsd [new file with mode: 0644]

index 9dc2ef7..e4a173d 100644 (file)
@@ -13,10 +13,6 @@ language: php
 # - Required for non-buggy xml library for XmlTypeCheck/UploadBaseTest (T75176).
 dist: trusty
 
-git:
-  depth: 3
-  quiet: true
-
 # Cache NPM and Composer directories
 # <https://docs.travis-ci.com/user/caching/>
 cache:
index 9d5d49e..6291bfc 100644 (file)
                                <!-- This isn't a good idea; we should be using "ID" instead of "NMTOKEN" -->
                                <!-- However, "NMTOKEN" is strictest definition that is both compatible with existing -->
                                <!-- usage ([0-9]+) and with the "ID" type. -->
-                               <attribute name="id" type="NMTOKEN" />
+                               <attribute name="id" use="optional" type="NMTOKEN" />
                                <attribute name="bytes" use="optional" type="nonNegativeInteger" />
                        </extension>
                </simpleContent>
index 6216046..be598ea 100644 (file)
@@ -98,11 +98,14 @@ class ContentSecurityPolicy {
         *
         * @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE
         * @return string Name of http header
+        * @throws UnexpectedValueException
         */
        private function getHeaderName( $reportOnly ) {
                if ( $reportOnly === self::REPORT_ONLY_MODE ) {
                        return 'Content-Security-Policy-Report-Only';
-               } elseif ( $reportOnly === self::FULL_MODE ) {
+               }
+
+               if ( $reportOnly === self::FULL_MODE ) {
                        return 'Content-Security-Policy';
                }
                throw new UnexpectedValueException( $reportOnly );
@@ -111,7 +114,8 @@ class ContentSecurityPolicy {
        /**
         * Determine what CSP policies to set for this page
         *
-        * @param array|bool $config Policy configuration (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
+        * @param array|bool $policyConfig Policy configuration
+        *   (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
         * @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE
         * @return string Policy directives, or empty string for no policy.
         */
@@ -152,8 +156,8 @@ class ContentSecurityPolicy {
                        }
                }
                // Note: default on if unspecified.
-               if ( !isset( $policyConfig['unsafeFallback'] )
-                       || $policyConfig['unsafeFallback'] )
+               if ( !isset( $policyConfig['unsafeFallback'] )
+                       || $policyConfig['unsafeFallback']
                ) {
                        // unsafe-inline should be ignored on browsers
                        // that support 'nonce-foo' sources.
index 68d7846..34b2796 100644 (file)
@@ -9041,6 +9041,16 @@ $wgOriginTrials = [];
  */
 $wgPriorityHints = false;
 
+/**
+ * Enable Element Timing.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var bool
+ */
+$wgElementTiming = false;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index c9ebfa8..ba4c6e8 100644 (file)
@@ -108,14 +108,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
                $result = [];
                if ( $params['entirewatchlist'] ) {
                        // Entire watchlist mode: Just update the thing and return a success indicator
-                       if ( is_null( $timestamp ) ) {
-                               $watchedItemStore->resetAllNotificationTimestampsForUser( $user );
-                       } else {
-                               $watchedItemStore->setNotificationTimestampsForUser(
-                                       $user,
-                                       $timestamp
-                               );
-                       }
+                       $watchedItemStore->resetAllNotificationTimestampsForUser( $user, $timestamp );
 
                        $result['notificationtimestamp'] = is_null( $timestamp )
                                ? ''
index 2c5b583..12ecd74 100644 (file)
@@ -542,7 +542,7 @@ class ApiUpload extends ApiBase {
                }
 
                // Check blocks
-               if ( $user->isBlocked() ) {
+               if ( $user->isBlockedFromUpload() ) {
                        $this->dieBlocked( $user->getBlock() );
                }
 
index 94c1351..b71580a 100644 (file)
 use MediaWiki\MediaWikiServices;
 
 /**
- * Job for clearing all of the "last viewed" timestamps for a user's watchlist
+ * Job for clearing all of the "last viewed" timestamps for a user's watchlist, or setting them all
+ * to the same value.
  *
  * Job parameters include:
  *   - userId: affected user ID [required]
  *   - casTime: UNIX timestamp of the event that triggered this job [required]
+ *   - timestamp: value to set all of the "last viewed" timestamps to [optional, defaults to null]
  *
  * @ingroup JobQueue
  * @since 1.31
@@ -38,7 +40,7 @@ class ClearWatchlistNotificationsJob extends Job {
                static $required = [ 'userId', 'casTime' ];
                $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
                if ( $missing != '' ) {
-                       throw new InvalidArgumentException( "Missing paramter(s) $missing" );
+                       throw new InvalidArgumentException( "Missing parameter(s) $missing" );
                }
 
                $this->removeDuplicates = true;
@@ -51,29 +53,48 @@ class ClearWatchlistNotificationsJob extends Job {
 
                $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
                $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+               $timestamp = $this->params['timestamp'] ?? null;
+               if ( $timestamp === null ) {
+                       $timestampCond = 'wl_notificationtimestamp IS NOT NULL';
+               } else {
+                       $timestamp = $dbw->timestamp( $timestamp );
+                       $timestampCond = 'wl_notificationtimestamp != ' . $dbw->addQuotes( $timestamp ) .
+                               ' OR wl_notificationtimestamp IS NULL';
+               }
+               // New notifications since the reset should not be cleared
+               $casTimeCond = 'wl_notificationtimestamp < ' .
+                       $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) ) .
+                       ' OR wl_notificationtimestamp IS NULL';
 
-               $asOfTimes = array_unique( $dbw->selectFieldValues(
-                       'watchlist',
-                       'wl_notificationtimestamp',
-                       [ 'wl_user' => $this->params['userId'], 'wl_notificationtimestamp IS NOT NULL' ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'wl_notificationtimestamp DESC' ]
-               ) );
-
-               foreach ( array_chunk( $asOfTimes, $rowsPerQuery ) as $asOfTimeBatch ) {
-                       $dbw->update(
+               $firstBatch = true;
+               do {
+                       $idsToUpdate = $dbw->selectFieldValues(
                                'watchlist',
-                               [ 'wl_notificationtimestamp' => null ],
+                               'wl_id',
                                [
                                        'wl_user' => $this->params['userId'],
-                                       'wl_notificationtimestamp' => $asOfTimeBatch,
-                                       // New notifications since the reset should not be cleared
-                                       'wl_notificationtimestamp < ' .
-                                               $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) )
+                                       $timestampCond,
+                                       $casTimeCond,
                                ],
-                               __METHOD__
+                               __METHOD__,
+                               [ 'LIMIT' => $rowsPerQuery ]
                        );
-                       $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
-               }
+                       if ( $idsToUpdate ) {
+                               $dbw->update(
+                                       'watchlist',
+                                       [ 'wl_notificationtimestamp' => $timestamp ],
+                                       [
+                                               'wl_id' => $idsToUpdate,
+                                               // For paranoia, enforce the CAS time condition here too
+                                               $casTimeCond
+                                       ],
+                                       __METHOD__
+                               );
+                               if ( !$firstBatch ) {
+                                       $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+                               }
+                               $firstBatch = false;
+                       }
+               } while ( $idsToUpdate );
        }
 }
index 7049df5..f2bc01d 100644 (file)
@@ -216,6 +216,11 @@ class DatabaseSqlite extends Database {
                        # Enforce LIKE to be case sensitive, just like MySQL
                        $this->query( 'PRAGMA case_sensitive_like = 1' );
 
+                       $sync = $this->sessionVars['synchronous'] ?? null;
+                       if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL' ], true ) ) {
+                               $this->query( "PRAGMA synchronous = $sync" );
+                       }
+
                        return $this->conn;
                }
 
index 87b4be7..48ea4a5 100644 (file)
@@ -358,7 +358,7 @@ class ThumbnailImage extends MediaTransformOutput {
         * @return string
         */
        function toHtml( $options = [] ) {
-               global $wgPriorityHints;
+               global $wgPriorityHints, $wgElementTiming;
 
                if ( count( func_get_args() ) == 2 ) {
                        throw new MWException( __METHOD__ . ' called in the old style' );
@@ -374,12 +374,19 @@ class ThumbnailImage extends MediaTransformOutput {
                        'decoding' => 'async',
                ];
 
+               $elementTimingName = 'thumbnail';
+
                if ( $wgPriorityHints
                        && !self::$firstNonIconImageRendered
                        && $this->width * $this->height > 100 * 100 ) {
                        self::$firstNonIconImageRendered = true;
 
                        $attribs['importance'] = 'high';
+                       $elementTimingName = 'thumbnail-high';
+               }
+
+               if ( $wgElementTiming ) {
+                       $attribs['elementtiming'] = $elementTimingName;
                }
 
                if ( !empty( $options['custom-url-link'] ) ) {
index 6611e20..f925038 100644 (file)
@@ -192,7 +192,6 @@ class BlockLevelPass {
                # happening here is handling of block-level elements p, pre,
                # and making lists from lines starting with * # : etc.
                $textLines = StringUtils::explode( "\n", $text );
-               $lineCount = substr_count( $text, "\n" ) + 1;
 
                $lastPrefix = $output = '';
                $this->DTopen = $inBlockElem = false;
@@ -200,7 +199,11 @@ class BlockLevelPass {
                $pendingPTag = false;
                $inBlockquote = false;
 
-               foreach ( $textLines as $i => $inputLine ) {
+               for ( $textLines->rewind(); $textLines->valid(); ) {
+                       $inputLine = $textLines->current();
+                       $textLines->next();
+                       $notLastLine = $textLines->valid();
+
                        # Fix up $lineStart
                        if ( !$this->lineStart ) {
                                $output .= $inputLine;
@@ -407,7 +410,7 @@ class BlockLevelPass {
                                        $output .= $t;
                                        // Add a newline if there's an open paragraph
                                        // or we've yet to reach the last line.
-                                       if ( $i < $lineCount - 1 || $this->hasOpenParagraph() ) {
+                                       if ( $notLastLine || $this->hasOpenParagraph() ) {
                                                $output .= "\n";
                                        }
                                } else {
index e092859..8aca689 100644 (file)
@@ -211,6 +211,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                                }
                        }
                }
+
+               $pageSeenKey = $this->getPageSeenTimestampsKey( $user );
+               $this->latestUpdateCache->delete( $pageSeenKey );
+               $this->stash->delete( $pageSeenKey );
        }
 
        /**
@@ -805,36 +809,64 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
+        * Set the "last viewed" timestamps for certain titles on a user's watchlist.
+        *
+        * If the $targets parameter is omitted or set to [], this method simply wraps
+        * resetAllNotificationTimestampsForUser(), and in that case you should instead call that method
+        * directly; support for omitting $targets is for backwards compatibility.
+        *
+        * If $targets is omitted or set to [], timestamps will be updated for every title on the user's
+        * watchlist, and this will be done through a DeferredUpdate. If $targets is a non-empty array,
+        * only the specified titles will be updated, and this will be done immediately (not deferred).
+        *
         * @since 1.27
         * @param User $user
-        * @param string|int $timestamp
-        * @param LinkTarget[] $targets
+        * @param string|int $timestamp Value to set the "last viewed" timestamp to (null to clear)
+        * @param LinkTarget[] $targets Titles to set the timestamp for; [] means the entire watchlist
         * @return bool
         */
        public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
                // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
+               if ( $user->isAnon() || $this->readOnlyMode->isReadOnly() ) {
                        return false;
                }
 
-               $dbw = $this->getConnectionRef( DB_MASTER );
-
-               $conds = [ 'wl_user' => $user->getId() ];
-               if ( $targets ) {
-                       $batch = new LinkBatch( $targets );
-                       $conds[] = $batch->constructSet( 'wl', $dbw );
+               if ( !$targets ) {
+                       // Backwards compatibility
+                       $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
+                       return true;
                }
 
+               $rows = $this->getTitleDbKeysGroupedByNamespace( $targets );
+
+               $dbw = $this->getConnectionRef( DB_MASTER );
                if ( $timestamp !== null ) {
                        $timestamp = $dbw->timestamp( $timestamp );
                }
+               $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
+               $affectedSinceWait = 0;
 
-               $dbw->update(
-                       'watchlist',
-                       [ 'wl_notificationtimestamp' => $timestamp ],
-                       $conds,
-                       __METHOD__
-               );
+               // Batch update items per namespace
+               foreach ( $rows as $namespace => $namespaceTitles ) {
+                       $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
+                       foreach ( $rowBatches as $toUpdate ) {
+                               $dbw->update(
+                                       'watchlist',
+                                       [ 'wl_notificationtimestamp' => $timestamp ],
+                                       [
+                                               'wl_user' => $user->getId(),
+                                               'wl_namespace' => $namespace,
+                                               'wl_title' => $toUpdate
+                                       ]
+                               );
+                               $affectedSinceWait += $dbw->affectedRows();
+                               // Wait for replication every time we've touched updateRowsPerQuery rows
+                               if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
+                                       $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+                                       $affectedSinceWait = 0;
+                               }
+                       }
+               }
 
                $this->uncacheUser( $user );
 
@@ -859,7 +891,13 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                return $timestamp;
        }
 
-       public function resetAllNotificationTimestampsForUser( User $user ) {
+       /**
+        * Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user
+        * to the same value.
+        * @param User $user
+        * @param string|int|null $timestamp Value to set all timestamps to, null to clear them
+        */
+       public function resetAllNotificationTimestampsForUser( User $user, $timestamp = null ) {
                // Only loggedin user can have a watchlist
                if ( $user->isAnon() ) {
                        return;
@@ -868,7 +906,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                // If the page is watched by the user (or may be watched), update the timestamp
                $job = new ClearWatchlistNotificationsJob(
                        $user->getUserPage(),
-                       [ 'userId'  => $user->getId(), 'casTime' => time() ]
+                       [ 'userId'  => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time() ]
                );
 
                // Try to run this post-send
@@ -1191,7 +1229,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param TitleValue[] $titles
+        * @param LinkTarget[] $titles
         * @return array
         */
        private function getTitleDbKeysGroupedByNamespace( array $titles ) {
index c32844c..3ca6632 100644 (file)
@@ -6,7 +6,7 @@
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2019-03-14T00:52:20Z
+ * Date: 2019-03-20T23:07:02Z
  */
 ( function ( OO ) {
 
@@ -7470,6 +7470,7 @@ OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
        this.$element
                .addClass( 'oo-ui-menuSectionOptionWidget' )
                .removeAttr( 'role aria-selected' );
+       this.selected = false;
 };
 
 /* Setup */
index 680b8c3..861111a 100644 (file)
@@ -180,6 +180,7 @@ $wgAutoloadClasses += [
        'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
 
        # tests/phpunit/maintenance
+       'MediaWiki\Tests\Maintenance\DumpAsserter' => "$testDir/phpunit/maintenance/DumpAsserter.php",
        'MediaWiki\Tests\Maintenance\DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php",
        'MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase' => "$testDir/phpunit/maintenance/MaintenanceBaseTestCase.php",
 
index 79ce634..1ef0c91 100644 (file)
@@ -130,4 +130,18 @@ trait PHPUnit4And6Compat {
                        // ->disallowMockingUnknownTypes()
                        ->getMock();
        }
+
+       /**
+        * Marks the current test as risky. This
+        * is a forward port of the markAsRisky function that
+        * was introduced in PHPUnit 5.7.6.
+        */
+       public function markAsRisky() {
+               if ( is_callable( 'parent::markAsRisky' ) ) {
+                       return parent::markAsRisky();
+               }
+
+               // "risky" tests are not supported in phpunit 4, so just ignore
+       }
+
 }
index 6a383a2..20dbedb 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @author Addshore
@@ -199,19 +200,28 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
 
                // setNotificationTimestampsForUser specifying a title
                $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] )
+                       $store->setNotificationTimestampsForUser( $user, '20100202020202', [ $title ] )
                );
                $this->assertEquals(
-                       '20200202020202',
+                       '20100202020202',
                        $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
                );
 
                // setNotificationTimestampsForUser not specifying a title
+               // This will try to use a DeferredUpdate; disable that
+               $mockCallback = function ( $callback ) {
+                       $callback();
+               };
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
                $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, '20210202020202' )
+                       $store->setNotificationTimestampsForUser( $user, '20110202020202' )
                );
+               // Because the operation above is normally deferred, it doesn't clear the cache
+               // Clear the cache manually
+               $wrappedStore = TestingAccessWrapper::newFromObject( $store );
+               $wrappedStore->uncacheUser( $user );
                $this->assertEquals(
-                       '20210202020202',
+                       '20110202020202',
                        $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
                );
        }
index 6249c49..a6b2162 100644 (file)
@@ -120,6 +120,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mock->expects( $this->any() )
                        ->method( 'getId' )
                        ->will( $this->returnValue( $id ) );
+               $mock->expects( $this->any() )
+                       ->method( 'getUserPage' )
+                       ->will( $this->returnValue( Title::makeTitle( NS_USER, 'MockUser' ) ) );
                return $mock;
        }
 
@@ -2628,59 +2631,46 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $user = $this->getMockNonAnonUserWithId( 1 );
                $timestamp = '20100101010101';
 
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( true ) );
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-
                $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
+                       $this->getMockLBFactory( $this->getMockDb() ),
                        $this->getMockJobQueueGroup(),
                        $this->getMockCache(),
                        $this->getMockReadOnlyMode()
                );
 
+               // Note: This does not actually assert the job is correct
+               $callableCallCounter = 0;
+               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+                       $callableCallCounter++;
+                       $this->assertInternalType( 'callable', $callable );
+               };
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
                $this->assertTrue(
                        $store->setNotificationTimestampsForUser( $user, $timestamp )
                );
+               $this->assertEquals( 1, $callableCallCounter );
        }
 
        public function testSetNotificationTimestampsForUser_nullTimestamp() {
                $user = $this->getMockNonAnonUserWithId( 1 );
                $timestamp = null;
 
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => null ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( true ) );
-               $mockDb->expects( $this->exactly( 0 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-
                $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
+                       $this->getMockLBFactory( $this->getMockDb() ),
                        $this->getMockJobQueueGroup(),
                        $this->getMockCache(),
                        $this->getMockReadOnlyMode()
                );
 
+               // Note: This does not actually assert the job is correct
+               $callableCallCounter = 0;
+               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+                       $callableCallCounter++;
+                       $this->assertInternalType( 'callable', $callable );
+               };
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
                $this->assertTrue(
                        $store->setNotificationTimestampsForUser( $user, $timestamp )
                );
@@ -2697,7 +2687,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with(
                                'watchlist',
                                [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
-                               [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
+                               [ 'wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => [ 'Foo', 'Bar' ] ]
                        )
                        ->will( $this->returnValue( true ) );
                $mockDb->expects( $this->exactly( 1 ) )
@@ -2706,13 +2696,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                return 'TS' . $value . 'TS';
                        } ) );
                $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'Foo' => 1, 'Bar' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+                       ->method( 'affectedRows' )
+                       ->will( $this->returnValue( 2 ) );
 
                $store = $this->newWatchedItemStore(
                        $this->getMockLBFactory( $mockDb ),
diff --git a/tests/phpunit/maintenance/DumpAsserter.php b/tests/phpunit/maintenance/DumpAsserter.php
new file mode 100644 (file)
index 0000000..5b4c6ef
--- /dev/null
@@ -0,0 +1,347 @@
+<?php
+
+namespace MediaWiki\Tests\Maintenance;
+
+use PHPUnit\Framework\Assert;
+use XMLReader;
+
+/**
+ * Helper for asserting the structure of an XML dump stream.
+ */
+class DumpAsserter {
+
+       /**
+        * Holds the XMLReader used for analyzing an XML dump
+        *
+        * @var XMLReader|null
+        */
+       protected $xml = null;
+
+       /**
+        * XML dump schema version
+        *
+        * @var string
+        */
+       protected $schemaVersion;
+
+       /**
+        * DumpAsserts constructor.
+        *
+        * @param string $schemaVersion see XML_DUMP_SCHEMA_VERSION_XX
+        */
+       public function __construct( $schemaVersion ) {
+               $this->schemaVersion = $schemaVersion;
+       }
+
+       /**
+        * Step the current XML reader until node end of given name is found.
+        *
+        * @param string $name Name of the closing element to look for
+        *   (e.g.: "mediawiki" when looking for </mediawiki>)
+        *
+        * @return bool True if the end node could be found. false otherwise.
+        */
+       public function skipToNodeEnd( $name ) {
+               while ( $this->xml->read() ) {
+                       if ( $this->xml->nodeType == XMLReader::END_ELEMENT &&
+                               $this->xml->name == $name
+                       ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Step the current XML reader to the first element start after the node
+        * end of a given name.
+        *
+        * @param string $name Name of the closing element to look for
+        *   (e.g.: "mediawiki" when looking for </mediawiki>)
+        *
+        * @return bool True if new element after the closing of $name could be
+        *   found. false otherwise.
+        */
+       public function skipPastNodeEnd( $name ) {
+               Assert::assertTrue( $this->skipToNodeEnd( $name ),
+                       "Skipping to end of $name" );
+               while ( $this->xml->read() ) {
+                       if ( $this->xml->nodeType == XMLReader::ELEMENT ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Opens an XML file to analyze and optionally skips past siteinfo.
+        *
+        * @param string $fname Name of file to analyze
+        * @param bool $skip_siteinfo (optional) If true, step the xml reader
+        *   to the first element after </siteinfo>
+        */
+       public function assertDumpStart( $fname, $skip_siteinfo = true ) {
+               $this->xml = new XMLReader();
+
+               Assert::assertTrue( $this->xml->open( $fname ),
+                       "Opening temporary file $fname via XMLReader failed" );
+               if ( $skip_siteinfo ) {
+                       Assert::assertTrue( $this->skipPastNodeEnd( "siteinfo" ),
+                               "Skipping past end of siteinfo" );
+               }
+       }
+
+       /**
+        * Asserts that the xml reader is at the final closing tag of an xml file and
+        * closes the reader.
+        *
+        * @param string $name (optional) the name of the final tag
+        *   (e.g.: "mediawiki" for </mediawiki>)
+        */
+       public function assertDumpEnd( $name = "mediawiki" ) {
+               $this->assertNodeEnd( $name, false );
+               if ( $this->xml->read() ) {
+                       $this->skipWhitespace();
+               }
+               Assert::assertEquals( $this->xml->nodeType, XMLReader::NONE,
+                       "No proper entity left to parse" );
+               $this->xml->close();
+       }
+
+       /**
+        * Steps the xml reader over white space
+        */
+       public function skipWhitespace() {
+               $cont = true;
+               while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE )
+                       || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) {
+                       $cont = $this->xml->read();
+               }
+       }
+
+       /**
+        * Asserts that the xml reader is at an element of given name, and optionally
+        * skips past it.
+        *
+        * @param string $name The name of the element to check for
+        *   (e.g.: "mediawiki" for <mediawiki>)
+        * @param bool $skip (optional) if true, skip past the found element
+        */
+       public function assertNodeStart( $name, $skip = true ) {
+               Assert::assertEquals( $name, $this->xml->name, "Node name" );
+               Assert::assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" );
+               if ( $skip ) {
+                       Assert::assertTrue( $this->xml->read(), "Skipping past start tag" );
+               }
+       }
+
+       /**
+        * Asserts that the xml reader is at an closing element of given name, and optionally
+        * skips past it.
+        *
+        * @param string $name The name of the closing element to check for
+        *   (e.g.: "mediawiki" for </mediawiki>)
+        * @param bool $skip (optional) if true, skip past the found element
+        */
+       public function assertNodeEnd( $name, $skip = true ) {
+               Assert::assertEquals( $name, $this->xml->name, "Node name" );
+               Assert::assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" );
+               if ( $skip ) {
+                       Assert::assertTrue( $this->xml->read(), "Skipping past end tag" );
+               }
+       }
+
+       /**
+        * Asserts that the xml reader is at an element of given tag that contains a given text,
+        * and skips over the element.
+        *
+        * @param string $name The name of the element to check for
+        *   (e.g.: "mediawiki" for <mediawiki>...</mediawiki>)
+        * @param string|bool $text If string, check if it equals the elements text.
+        *   If false, ignore the element's text
+        * @param bool $skip_ws (optional) if true, skip past white spaces that trail the
+        *   closing element.
+        */
+       public function assertTextNode( $name, $text, $skip_ws = true ) {
+               $this->assertNodeStart( $name );
+
+               if ( $text !== false ) {
+                       Assert::assertEquals( $text, $this->xml->value, "Text of node " . $name );
+               }
+               Assert::assertTrue( $this->xml->read(), "Skipping past processed text of " . $name );
+               $this->assertNodeEnd( $name );
+
+               if ( $skip_ws ) {
+                       $this->skipWhitespace();
+               }
+       }
+
+       /**
+        * Asserts that the xml reader is at the start of a page element and skips over the first
+        * tags, after checking them.
+        *
+        * Besides the opening page element, this function also checks for and skips over the
+        * title, ns, and id tags. Hence after this function, the xml reader is at the first
+        * revision of the current page.
+        *
+        * @param int $id Id of the page to assert
+        * @param int $ns Number of namespage to assert
+        * @param string $name Title of the current page
+        */
+       public function assertPageStart( $id, $ns, $name ) {
+               $this->assertNodeStart( "page" );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "title", $name );
+               $this->assertTextNode( "ns", $ns );
+               $this->assertTextNode( "id", $id );
+       }
+
+       /**
+        * Asserts that the xml reader is at the page's closing element and skips to the next
+        * element.
+        */
+       public function assertPageEnd() {
+               $this->assertNodeEnd( "page" );
+               $this->skipWhitespace();
+       }
+
+       /**
+        * Asserts that the xml reader is at a revision and checks its representation before
+        * skipping over it.
+        *
+        * @param int $id Id of the revision
+        * @param string $summary Summary of the revision
+        * @param int $text_id Id of the revision's text
+        * @param int $text_bytes Number of bytes in the revision's text
+        * @param string $text_sha1 The base36 SHA-1 of the revision's text
+        * @param string|bool $text (optional) The revision's string, or false to check for a
+        *            revision stub
+        * @param int|bool $parentid (optional) id of the parent revision
+        * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT)
+        * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT)
+        */
+       public function assertRevision( $id, $summary, $text_id, $text_bytes,
+               $text_sha1, $text = false, $parentid = false,
+               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT
+       ) {
+               $this->assertNodeStart( "revision" );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "id", $id );
+               if ( $parentid !== false ) {
+                       $this->assertTextNode( "parentid", $parentid );
+               }
+               $this->assertTextNode( "timestamp", false );
+
+               $this->assertNodeStart( "contributor" );
+               $this->skipWhitespace();
+               $this->assertTextNode( "ip", false );
+               $this->assertNodeEnd( "contributor" );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "comment", $summary );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "model", $model );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "format", $format );
+               $this->skipWhitespace();
+
+               if ( $this->xml->name == "text" ) {
+                       // note: <text> tag may occur here or at the very end.
+                       $text_found = true;
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               } else {
+                       $text_found = false;
+               }
+
+               $this->assertTextNode( "sha1", $text_sha1 );
+
+               if ( !$text_found ) {
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               }
+
+               $this->assertNodeEnd( "revision" );
+               $this->skipWhitespace();
+       }
+
+       public function assertText( $id, $text_id, $text_bytes, $text ) {
+               $this->assertNodeStart( "text", false );
+               if ( $text_bytes !== false ) {
+                       Assert::assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
+                               "Attribute 'bytes' of revision " . $id );
+               }
+
+               if ( $text === false ) {
+                       // Testing for a stub
+                       Assert::assertEquals( $this->xml->getAttribute( "id" ), $text_id,
+                               "Text id of revision " . $id );
+                       Assert::assertFalse( $this->xml->hasValue, "Revision has text" );
+                       Assert::assertTrue( $this->xml->read(), "Skipping text start tag" );
+                       if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT )
+                               && ( $this->xml->name == "text" )
+                       ) {
+                               $this->xml->read();
+                       }
+                       $this->skipWhitespace();
+               } else {
+                       // Testing for a real dump
+                       Assert::assertTrue( $this->xml->read(), "Skipping text start tag" );
+                       Assert::assertEquals( $text, $this->xml->value, "Text of revision " . $id );
+                       Assert::assertTrue( $this->xml->read(), "Skipping past text" );
+                       $this->assertNodeEnd( "text" );
+                       $this->skipWhitespace();
+               }
+       }
+
+       /**
+        * asserts that the xml reader is at the beginning of a log entry and skips over
+        * it while analyzing it.
+        *
+        * @param int $id Id of the log entry
+        * @param string $user_name User name of the log entry's performer
+        * @param int $user_id User id of the log entry 's performer
+        * @param string|null $comment Comment of the log entry. If null, the comment text is ignored.
+        * @param string $type Type of the log entry
+        * @param string $subtype Subtype of the log entry
+        * @param string $title Title of the log entry's target
+        * @param array $parameters (optional) unserialized data accompanying the log entry
+        */
+       public function assertLogItem( $id, $user_name, $user_id, $comment, $type,
+               $subtype, $title, $parameters = []
+       ) {
+               $this->assertNodeStart( "logitem" );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "id", $id );
+               $this->assertTextNode( "timestamp", false );
+
+               $this->assertNodeStart( "contributor" );
+               $this->skipWhitespace();
+               $this->assertTextNode( "username", $user_name );
+               $this->assertTextNode( "id", $user_id );
+               $this->assertNodeEnd( "contributor" );
+               $this->skipWhitespace();
+
+               if ( $comment !== null ) {
+                       $this->assertTextNode( "comment", $comment );
+               }
+               $this->assertTextNode( "type", $type );
+               $this->assertTextNode( "action", $subtype );
+               $this->assertTextNode( "logtitle", $title );
+
+               $this->assertNodeStart( "params" );
+               $parameters_xml = unserialize( $this->xml->value );
+               Assert::assertEquals( $parameters, $parameters_xml );
+               Assert::assertTrue( $this->xml->read(), "Skipping past processed text of params" );
+               $this->assertNodeEnd( "params" );
+               $this->skipWhitespace();
+
+               $this->assertNodeEnd( "logitem" );
+               $this->skipWhitespace();
+       }
+}
index 4b7a7eb..eebc201 100644 (file)
@@ -3,11 +3,12 @@
 namespace MediaWiki\Tests\Maintenance;
 
 use ContentHandler;
+use DOMDocument;
 use ExecutableFinder;
 use MediaWikiLangTestCase;
-use Page;
 use User;
-use XMLReader;
+use WikiExporter;
+use WikiPage;
 use MWException;
 
 /**
@@ -28,13 +29,6 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
         */
        protected $exceptionFromAddDBData = null;
 
-       /**
-        * Holds the XMLReader used for analyzing an XML dump
-        *
-        * @var XMLReader|null
-        */
-       protected $xml = null;
-
        /** @var bool|null Whether the 'gzip' utility is available */
        protected static $hasGzip = null;
 
@@ -58,7 +52,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
        /**
         * Adds a revision to a page, while returning the resuting revision's id
         *
-        * @param Page $page Page to add the revision to
+        * @param WikiPage $page Page to add the revision to
         * @param string $text Revisions text
         * @param string $summary Revisions summary
         * @param string $model The model ID (defaults to wikitext)
@@ -66,7 +60,12 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
         * @throws MWException
         * @return array
         */
-       protected function addRevision( Page $page, $text, $summary, $model = CONTENT_MODEL_WIKITEXT ) {
+       protected function addRevision(
+               WikiPage $page,
+               $text,
+               $summary,
+               $model = CONTENT_MODEL_WIKITEXT
+       ) {
                $status = $page->doEditContent(
                        ContentHandler::makeContent( $text, $page->getTitle(), $model ),
                        $summary
@@ -108,6 +107,36 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
                );
        }
 
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               if ( !function_exists( 'libxml_set_external_entity_loader' ) ) {
+                       return;
+               }
+
+               // The W3C is intentionally slow about returning schema files,
+               // see <https://www.w3.org/Help/Webmaster#slowdtd>.
+               // To work around that, we keep our own copies of the relevant schema files.
+               libxml_set_external_entity_loader(
+                       function ( $public, $system, $context ) {
+                               switch ( $system ) {
+                                       // if more schema files are needed, add them here.
+                                       case 'http://www.w3.org/2001/xml.xsd':
+                                               $file = __DIR__ . '/xml.xsd';
+                                               break;
+                                       default:
+                                               if ( is_file( $system ) ) {
+                                                       $file = $system;
+                                               } else {
+                                                       return null;
+                                               }
+                               }
+
+                               return $file;
+                       }
+               );
+       }
+
        /**
         * Default set up function.
         *
@@ -125,6 +154,21 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
                $this->setMwGlobals( 'wgUser', new User() );
        }
 
+       /**
+        * Returns the path to the XML schema file for the given schema version.
+        *
+        * @param string|null $schemaVersion
+        *
+        * @return string
+        */
+       protected function getXmlSchemaPath( $schemaVersion = null ) {
+               global $IP;
+
+               $schemaVersion = $schemaVersion ?: '0.10';
+
+               return "$IP/docs/export-$schemaVersion.xsd";
+       }
+
        /**
         * Checks for test output consisting only of lines containing ETA announcements
         */
@@ -152,266 +196,62 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
        }
 
        /**
-        * Step the current XML reader until node end of given name is found.
-        *
-        * @param string $name Name of the closing element to look for
-        *   (e.g.: "mediawiki" when looking for </mediawiki>)
+        * @param null|string $schemaVersion
         *
-        * @return bool True if the end node could be found. false otherwise.
+        * @return DumpAsserter
         */
-       protected function skipToNodeEnd( $name ) {
-               while ( $this->xml->read() ) {
-                       if ( $this->xml->nodeType == XMLReader::END_ELEMENT &&
-                               $this->xml->name == $name
-                       ) {
-                               return true;
-                       }
-               }
-
-               return false;
+       protected function getDumpAsserter( $schemaVersion = null ) {
+               $schemaVersion = $schemaVersion ?: WikiExporter::schemaVersion();
+               return new DumpAsserter( $schemaVersion );
        }
 
        /**
-        * Step the current XML reader to the first element start after the node
-        * end of a given name.
-        *
-        * @param string $name Name of the closing element to look for
-        *   (e.g.: "mediawiki" when looking for </mediawiki>)
-        *
-        * @return bool True if new element after the closing of $name could be
-        *   found. false otherwise.
+        * Checks an XML file against an XSD schema.
         */
-       protected function skipPastNodeEnd( $name ) {
-               $this->assertTrue( $this->skipToNodeEnd( $name ),
-                       "Skipping to end of $name" );
-               while ( $this->xml->read() ) {
-                       if ( $this->xml->nodeType == XMLReader::ELEMENT ) {
-                               return true;
-                       }
+       protected function assertDumpSchema( $fname, $schemaFile ) {
+               if ( !function_exists( 'libxml_use_internal_errors' ) ) {
+                       // Would be nice to leave a warning somehow.
+                       // We don't want to skip all of the test case that calls this, though.
+                       $this->markAsRisky();
+                       return;
                }
-
-               return false;
-       }
-
-       /**
-        * Opens an XML file to analyze and optionally skips past siteinfo.
-        *
-        * @param string $fname Name of file to analyze
-        * @param bool $skip_siteinfo (optional) If true, step the xml reader
-        *   to the first element after </siteinfo>
-        */
-       protected function assertDumpStart( $fname, $skip_siteinfo = true ) {
-               $this->xml = new XMLReader();
-               $this->assertTrue( $this->xml->open( $fname ),
-                       "Opening temporary file $fname via XMLReader failed" );
-               if ( $skip_siteinfo ) {
-                       $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ),
-                               "Skipping past end of siteinfo" );
+               if ( defined( 'HHVM_VERSION' ) ) {
+                       // In HHVM, loading a schema from a file is disabled per default.
+                       // This is controlled by hhvm.libxml.ext_entity_whitelist which
+                       // cannot be read with ini_get(), see
+                       // <https://docs.hhvm.com/hhvm/configuration/INI-settings#xml>.
+                       // Would be nice to leave a warning somehow.
+                       // We don't want to skip all of the test case that calls this, though.
+                       $this->markAsRisky();
+                       return;
                }
-       }
 
-       /**
-        * Asserts that the xml reader is at the final closing tag of an xml file and
-        * closes the reader.
-        *
-        * @param string $name (optional) the name of the final tag
-        *   (e.g.: "mediawiki" for </mediawiki>)
-        */
-       protected function assertDumpEnd( $name = "mediawiki" ) {
-               $this->assertNodeEnd( $name, false );
-               if ( $this->xml->read() ) {
-                       $this->skipWhitespace();
-               }
-               $this->assertEquals( $this->xml->nodeType, XMLReader::NONE,
-                       "No proper entity left to parse" );
-               $this->xml->close();
-       }
+               $xml = new DOMDocument();
+               $this->assertTrue( $xml->load( $fname ),
+                       "Opening temporary file $fname via DOMDocument failed" );
 
-       /**
-        * Steps the xml reader over white space
-        */
-       protected function skipWhitespace() {
-               $cont = true;
-               while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE )
-                       || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) {
-                       $cont = $this->xml->read();
-               }
-       }
+               // Don't throw
+               $oldLibXmlInternalErrors = libxml_use_internal_errors( true );
 
-       /**
-        * Asserts that the xml reader is at an element of given name, and optionally
-        * skips past it.
-        *
-        * @param string $name The name of the element to check for
-        *   (e.g.: "mediawiki" for <mediawiki>)
-        * @param bool $skip (optional) if true, skip past the found element
-        */
-       protected function assertNodeStart( $name, $skip = true ) {
-               $this->assertEquals( $name, $this->xml->name, "Node name" );
-               $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" );
-               if ( $skip ) {
-                       $this->assertTrue( $this->xml->read(), "Skipping past start tag" );
-               }
-       }
-
-       /**
-        * Asserts that the xml reader is at an closing element of given name, and optionally
-        * skips past it.
-        *
-        * @param string $name The name of the closing element to check for
-        *   (e.g.: "mediawiki" for </mediawiki>)
-        * @param bool $skip (optional) if true, skip past the found element
-        */
-       protected function assertNodeEnd( $name, $skip = true ) {
-               $this->assertEquals( $name, $this->xml->name, "Node name" );
-               $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" );
-               if ( $skip ) {
-                       $this->assertTrue( $this->xml->read(), "Skipping past end tag" );
-               }
-       }
-
-       /**
-        * Asserts that the xml reader is at an element of given tag that contains a given text,
-        * and skips over the element.
-        *
-        * @param string $name The name of the element to check for
-        *   (e.g.: "mediawiki" for <mediawiki>...</mediawiki>)
-        * @param string|bool $text If string, check if it equals the elements text.
-        *   If false, ignore the element's text
-        * @param bool $skip_ws (optional) if true, skip past white spaces that trail the
-        *   closing element.
-        */
-       protected function assertTextNode( $name, $text, $skip_ws = true ) {
-               $this->assertNodeStart( $name );
-
-               if ( $text !== false ) {
-                       $this->assertEquals( $text, $this->xml->value, "Text of node " . $name );
-               }
-               $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name );
-               $this->assertNodeEnd( $name );
-
-               if ( $skip_ws ) {
-                       $this->skipWhitespace();
-               }
-       }
-
-       /**
-        * Asserts that the xml reader is at the start of a page element and skips over the first
-        * tags, after checking them.
-        *
-        * Besides the opening page element, this function also checks for and skips over the
-        * title, ns, and id tags. Hence after this function, the xml reader is at the first
-        * revision of the current page.
-        *
-        * @param int $id Id of the page to assert
-        * @param int $ns Number of namespage to assert
-        * @param string $name Title of the current page
-        */
-       protected function assertPageStart( $id, $ns, $name ) {
-               $this->assertNodeStart( "page" );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "title", $name );
-               $this->assertTextNode( "ns", $ns );
-               $this->assertTextNode( "id", $id );
-       }
+               // NOTE: if this reports "Invalid Schema", the schema may be referencing an external
+               // entity (typically, another schema) that needs to be mapped in the
+               // libxml_set_external_entity_loader callback defined in setUpBeforeClass() above!
+               // Or $schemaFile doesn't point to a schema file, or the schema is indeed just broken.
+               if ( !$xml->schemaValidate( $schemaFile ) ) {
+                       $errorText = '';
 
-       /**
-        * Asserts that the xml reader is at the page's closing element and skips to the next
-        * element.
-        */
-       protected function assertPageEnd() {
-               $this->assertNodeEnd( "page" );
-               $this->skipWhitespace();
-       }
-
-       /**
-        * Asserts that the xml reader is at a revision and checks its representation before
-        * skipping over it.
-        *
-        * @param int $id Id of the revision
-        * @param string $summary Summary of the revision
-        * @param int $text_id Id of the revision's text
-        * @param int $text_bytes Number of bytes in the revision's text
-        * @param string $text_sha1 The base36 SHA-1 of the revision's text
-        * @param string|bool $text (optional) The revision's string, or false to check for a
-        *            revision stub
-        * @param int|bool $parentid (optional) id of the parent revision
-        * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT)
-        * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT)
-        */
-       protected function assertRevision( $id, $summary, $text_id, $text_bytes,
-               $text_sha1, $text = false, $parentid = false,
-               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT
-       ) {
-               $this->assertNodeStart( "revision" );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "id", $id );
-               if ( $parentid !== false ) {
-                       $this->assertTextNode( "parentid", $parentid );
-               }
-               $this->assertTextNode( "timestamp", false );
-
-               $this->assertNodeStart( "contributor" );
-               $this->skipWhitespace();
-               $this->assertTextNode( "ip", false );
-               $this->assertNodeEnd( "contributor" );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "comment", $summary );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "model", $model );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "format", $format );
-               $this->skipWhitespace();
-
-               if ( $this->xml->name == "text" ) {
-                       // note: <text> tag may occur here or at the very end.
-                       $text_found = true;
-                       $this->assertText( $id, $text_id, $text_bytes, $text );
-               } else {
-                       $text_found = false;
-               }
+                       foreach ( libxml_get_errors() as $error ) {
+                               $errorText .= "\nline {$error->line}: {$error->message}";
+                       }
 
-               $this->assertTextNode( "sha1", $text_sha1 );
+                       libxml_clear_errors();
 
-               if ( !$text_found ) {
-                       $this->assertText( $id, $text_id, $text_bytes, $text );
+                       $this->fail(
+                               "Failed asserting that $fname conforms to the schema in $schemaFile:\n$errorText"
+                       );
                }
 
-               $this->assertNodeEnd( "revision" );
-               $this->skipWhitespace();
+               libxml_use_internal_errors( $oldLibXmlInternalErrors );
        }
 
-       protected function assertText( $id, $text_id, $text_bytes, $text ) {
-               $this->assertNodeStart( "text", false );
-               if ( $text_bytes !== false ) {
-                       $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
-                               "Attribute 'bytes' of revision " . $id );
-               }
-
-               if ( $text === false ) {
-                       // Testing for a stub
-                       $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id,
-                               "Text id of revision " . $id );
-                       $this->assertFalse( $this->xml->hasValue, "Revision has text" );
-                       $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
-                       if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT )
-                               && ( $this->xml->name == "text" )
-                       ) {
-                               $this->xml->read();
-                       }
-                       $this->skipWhitespace();
-               } else {
-                       // Testing for a real dump
-                       $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
-                       $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id );
-                       $this->assertTrue( $this->xml->read(), "Skipping past text" );
-                       $this->assertNodeEnd( "text" );
-                       $this->skipWhitespace();
-               }
-       }
 }
index 38a513e..0d4bc56 100644 (file)
@@ -130,45 +130,46 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
 
                // Checking for correctness of the dumped data
-               $this->assertDumpStart( $nameFull );
+               $asserter = $this->getDumpAsserter();
+               $asserter->assertDumpStart( $nameFull );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+               $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+               $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
                        $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
                        "BackupDumperTestP1Text1" );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
-               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+               $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+               $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
                        "BackupDumperTestP2Text1" );
-               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+               $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
                        "BackupDumperTestP2Text2", $this->revId2_1 );
-               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+               $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
                        "BackupDumperTestP2Text3", $this->revId2_2 );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+               $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
                        "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+               $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+               $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
                        $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
                        "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
                        false,
                        "BackupTextPassTestModel",
                        "text/plain" );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
        function testPrefetchPlain() {
@@ -202,49 +203,50 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
 
                // Checking for correctness of the dumped data
-               $this->assertDumpStart( $nameFull );
+               $asserter = $this->getDumpAsserter();
+               $asserter->assertDumpStart( $nameFull );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+               $asserter->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
                // Prefetch kicks in. This is still the SHA-1 of the original text,
                // But the actual text (with different SHA-1) comes from prefetch.
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+               $asserter->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
                        $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
                        "Prefetch_________1Text1" );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
-               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+               $asserter->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+               $asserter->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
                        "BackupDumperTestP2Text1" );
-               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+               $asserter->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
                        "BackupDumperTestP2Text2", $this->revId2_1 );
                // Prefetch kicks in. This is still the SHA-1 of the original text,
                // But the actual text (with different SHA-1) comes from prefetch.
-               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+               $asserter->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
                        "Prefetch_________2Text3", $this->revId2_2 );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+               $asserter->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
                        "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+               $asserter->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+               $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
                        $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
                        "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
                        false,
                        "BackupTextPassTestModel",
                        "text/plain" );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
        /**
@@ -329,6 +331,8 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                $lookingForPage = 1;
                $checkpointFiles = 0;
 
+               $asserter = $this->getDumpAsserter();
+
                // Each run of the following loop body tries to handle exactly 1 /page/ (not
                // iteration of stub content). $i is only increased after having treated page 4.
                for ( $i = 0; $i < $iterations; ) {
@@ -346,7 +350,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                                if ( $checkpointFormat == "gzip" ) {
                                        $this->gunzip( $nameOutputDir . "/" . $fname );
                                }
-                               $this->assertDumpStart( $nameOutputDir . "/" . $fname );
+                               $asserter->assertDumpStart( $nameOutputDir . "/" . $fname );
                                $fileOpened = true;
                                $checkpointFiles++;
                        }
@@ -355,51 +359,90 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                        switch ( $lookingForPage ) {
                                case 1:
                                        // Page 1
-                                       $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
-                                               "BackupDumperTestP1" );
-                                       $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
-                                               $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
-                                               "BackupDumperTestP1Text1" );
-                                       $this->assertPageEnd();
+                                       $asserter->assertPageStart(
+                                               $this->pageId1 + $i * self::$numOfPages,
+                                               NS_MAIN,
+                                               "BackupDumperTestP1"
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId1_1 + $i * self::$numOfRevs,
+                                               "BackupDumperTestP1Summary1",
+                                               $this->textId1_1,
+                                               false,
+                                               "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+                                               "BackupDumperTestP1Text1"
+                                       );
+                                       $asserter->assertPageEnd();
 
                                        $lookingForPage = 2;
                                        break;
 
                                case 2:
                                        // Page 2
-                                       $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
-                                               "BackupDumperTestP2" );
-                                       $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
-                                               $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                                               "BackupDumperTestP2Text1" );
-                                       $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
-                                               $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                                               "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
-                                       $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
-                                               $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                                               "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
-                                       $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
+                                       $asserter->assertPageStart(
+                                               $this->pageId2 + $i * self::$numOfPages,
+                                               NS_MAIN,
+                                               "BackupDumperTestP2"
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId2_1 + $i * self::$numOfRevs,
+                                               "BackupDumperTestP2Summary1",
+                                               $this->textId2_1,
+                                               false,
+                                               "jprywrymfhysqllua29tj3sc7z39dl2",
+                                               "BackupDumperTestP2Text1"
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId2_2 + $i * self::$numOfRevs,
+                                               "BackupDumperTestP2Summary2",
+                                               $this->textId2_2,
+                                               false,
+                                               "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+                                               "BackupDumperTestP2Text2",
+                                               $this->revId2_1 + $i * self::$numOfRevs
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId2_3 + $i * self::$numOfRevs,
+                                               "BackupDumperTestP2Summary3",
+                                               $this->textId2_3,
+                                               false,
+                                               "jfunqmh1ssfb8rs43r19w98k28gg56r",
+                                               "BackupDumperTestP2Text3",
+                                               $this->revId2_2 + $i * self::$numOfRevs
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId2_4 + $i * self::$numOfRevs,
                                                "BackupDumperTestP2Summary4 extra",
-                                               $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                                               $this->textId2_4,
+                                               false,
+                                               "6o1ciaxa6pybnqprmungwofc4lv00wv",
                                                "BackupDumperTestP2Text4 some additional Text",
-                                               $this->revId2_3 + $i * self::$numOfRevs );
-                                       $this->assertPageEnd();
+                                               $this->revId2_3 + $i * self::$numOfRevs
+                                       );
+                                       $asserter->assertPageEnd();
 
                                        $lookingForPage = 4;
                                        break;
 
                                case 4:
                                        // Page 4
-                                       $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
-                                               "Talk:BackupDumperTestP1" );
-                                       $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
+                                       $asserter->assertPageStart(
+                                               $this->pageId4 + $i * self::$numOfPages,
+                                               NS_TALK,
+                                               "Talk:BackupDumperTestP1"
+                                       );
+                                       $asserter->assertRevision(
+                                               $this->revId4_1 + $i * self::$numOfRevs,
                                                "Talk BackupDumperTestP1 Summary1",
-                                               $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+                                               $this->textId4_1,
+                                               false,
+                                               "nktofwzd0tl192k3zfepmlzxoax1lpe",
                                                "TALK ABOUT BACKUPDUMPERTESTP1 TEXT1",
                                                false,
                                                "BackupTextPassTestModel",
-                                               "text/plain" );
-                                       $this->assertPageEnd();
+                                               "text/plain"
+                                       );
+                                       $asserter->assertPageEnd();
 
                                        $lookingForPage = 1;
 
@@ -415,7 +458,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
                        if ( $this->xml->nodeType == XMLReader::END_ELEMENT
                                && $this->xml->name == "mediawiki"
                        ) {
-                               $this->assertDumpEnd();
+                               $asserter->assertDumpEnd();
                                $fileOpened = false;
                        }
                }
index 9357451..811f1ee 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Tests\Maintenance;
 
+use Exception;
 use MediaWiki\MediaWikiServices;
 use DumpBackup;
 use ManualLogEntry;
@@ -98,53 +99,6 @@ class BackupDumperLoggerTest extends DumpTestCase {
                }
        }
 
-       /**
-        * asserts that the xml reader is at the beginning of a log entry and skips over
-        * it while analyzing it.
-        *
-        * @param int $id Id of the log entry
-        * @param string $user_name User name of the log entry's performer
-        * @param int $user_id User id of the log entry 's performer
-        * @param string|null $comment Comment of the log entry. If null, the comment text is ignored.
-        * @param string $type Type of the log entry
-        * @param string $subtype Subtype of the log entry
-        * @param string $title Title of the log entry's target
-        * @param array $parameters (optional) unserialized data accompanying the log entry
-        */
-       private function assertLogItem( $id, $user_name, $user_id, $comment, $type,
-               $subtype, $title, $parameters = []
-       ) {
-               $this->assertNodeStart( "logitem" );
-               $this->skipWhitespace();
-
-               $this->assertTextNode( "id", $id );
-               $this->assertTextNode( "timestamp", false );
-
-               $this->assertNodeStart( "contributor" );
-               $this->skipWhitespace();
-               $this->assertTextNode( "username", $user_name );
-               $this->assertTextNode( "id", $user_id );
-               $this->assertNodeEnd( "contributor" );
-               $this->skipWhitespace();
-
-               if ( $comment !== null ) {
-                       $this->assertTextNode( "comment", $comment );
-               }
-               $this->assertTextNode( "type", $type );
-               $this->assertTextNode( "action", $subtype );
-               $this->assertTextNode( "logtitle", $title );
-
-               $this->assertNodeStart( "params" );
-               $parameters_xml = unserialize( $this->xml->value );
-               $this->assertEquals( $parameters, $parameters_xml );
-               $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" );
-               $this->assertNodeEnd( "params" );
-               $this->skipWhitespace();
-
-               $this->assertNodeEnd( "logitem" );
-               $this->skipWhitespace();
-       }
-
        function testPlain() {
                // Preparing the dump
                $fname = $this->getNewTempFile();
@@ -159,9 +113,12 @@ class BackupDumperLoggerTest extends DumpTestCase {
                $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT );
 
                // Analyzing the dumped data
-               $this->assertDumpStart( $fname );
+               $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() );
+
+               $asserter = $this->getDumpAsserter();
+               $asserter->assertDumpStart( $fname );
 
-               $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+               $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA",
                        $this->userId1, null, "type", "subtype", "PageA" );
 
                $contLang = MediaWikiServices::getInstance()->getContentLanguage();
@@ -169,15 +126,15 @@ class BackupDumperLoggerTest extends DumpTestCase {
                $namespace = $contLang->getNsText( NS_TALK );
                $this->assertInternalType( 'string', $namespace );
                $this->assertGreaterThan( 0, strlen( $namespace ) );
-               $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+               $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB",
                        $this->userId2, "SomeComment", "supress", "delete",
                        $namespace . ":PageB" );
 
-               $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+               $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB",
                        $this->userId2, "SomeOtherComment", "move", "delete",
                        "PageA", [ 'key1' => 1, 3 => 'value3' ] );
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
        function testXmlDumpsBackupUseCaseLogging() {
@@ -211,9 +168,12 @@ class BackupDumperLoggerTest extends DumpTestCase {
                // Analyzing the dumped data
                $this->gunzip( $fname );
 
-               $this->assertDumpStart( $fname );
+               $this->assertDumpSchema( $fname, $this->getXmlSchemaPath() );
+
+               $asserter = $this->getDumpAsserter();
+               $asserter->assertDumpStart( $fname );
 
-               $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+               $asserter->assertLogItem( $this->logId1, "BackupDumperLogUserA",
                        $this->userId1, null, "type", "subtype", "PageA" );
 
                $contLang = MediaWikiServices::getInstance()->getContentLanguage();
@@ -221,15 +181,15 @@ class BackupDumperLoggerTest extends DumpTestCase {
                $namespace = $contLang->getNsText( NS_TALK );
                $this->assertInternalType( 'string', $namespace );
                $this->assertGreaterThan( 0, strlen( $namespace ) );
-               $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+               $asserter->assertLogItem( $this->logId2, "BackupDumperLogUserB",
                        $this->userId2, "SomeComment", "supress", "delete",
                        $namespace . ":PageB" );
 
-               $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+               $asserter->assertLogItem( $this->logId3, "BackupDumperLogUserB",
                        $this->userId2, "SomeOtherComment", "move", "delete",
                        "PageA", [ 'key1' => 1, 3 => 'value3' ] );
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
 
                // Currently, no reporting is implemented. Alert via failure, once
                // this changes.
index c37be4e..afe8c4b 100644 (file)
@@ -3,6 +3,7 @@
 namespace MediaWiki\Tests\Maintenance;
 
 use DumpBackup;
+use Exception;
 use MediaWiki\MediaWikiServices;
 use MediaWikiTestCase;
 use MWException;
@@ -169,12 +170,19 @@ class BackupDumperPageTest extends DumpTestCase {
                return $dumper;
        }
 
-       function testFullTextPlain() {
+       public function schemaVersionProvider() {
+               yield [ '0.10' ];
+       }
+
+       /**
+        * @dataProvider schemaVersionProvider
+        */
+       function testFullTextPlain( $schemaVersion ) {
                // Preparing the dump
                $fname = $this->getNewTempFile();
 
                $dumper = $this->newDumpBackup(
-                       [ '--full', '--quiet', '--output', 'file:' . $fname ],
+                       [ '--full', '--quiet', '--output', 'file:' . $fname, '--schema-version', $schemaVersion ],
                        $this->pageId1,
                        $this->pageId4 + 1
                );
@@ -183,54 +191,114 @@ class BackupDumperPageTest extends DumpTestCase {
                $dumper->execute();
 
                // Checking the dumped data
-               $this->assertDumpStart( $fname );
+               $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+
+               $asserter->assertDumpStart( $fname );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
-                       "BackupDumperTestP1Text1" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+                       "BackupDumperTestP1Text1"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
-                       $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2",
-                       "BackupDumperTestP2Text1" );
-               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
-                       $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                       "BackupDumperTestP2Text2", $this->revId2_1 );
-               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
-                       $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                       "BackupDumperTestP2Text3", $this->revId2_2 );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv",
-                       "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_1,
+                       "BackupDumperTestP2Summary1",
+                       $this->textId2_1,
+                       23,
+                       "jprywrymfhysqllua29tj3sc7z39dl2",
+                       "BackupDumperTestP2Text1"
+               );
+               $asserter->assertRevision(
+                       $this->revId2_2,
+                       "BackupDumperTestP2Summary2",
+                       $this->textId2_2,
+                       23,
+                       "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+                       "BackupDumperTestP2Text2",
+                       $this->revId2_1
+               );
+               $asserter->assertRevision(
+                       $this->revId2_3,
+                       "BackupDumperTestP2Summary3",
+                       $this->textId2_3,
+                       23,
+                       "jfunqmh1ssfb8rs43r19w98k28gg56r",
+                       "BackupDumperTestP2Text3",
+                       $this->revId2_2
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       "BackupDumperTestP2Text4 some additional Text",
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
                        $this->pageTitle4->getPrefixedText()
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
-                       $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe",
-                       "Talk about BackupDumperTestP1 Text1" );
-               $this->assertPageEnd();
+               $asserter->assertRevision(
+                       $this->revId4_1,
+                       "Talk BackupDumperTestP1 Summary1",
+                       $this->textId4_1,
+                       35,
+                       "nktofwzd0tl192k3zfepmlzxoax1lpe",
+                       "Talk about BackupDumperTestP1 Text1",
+                       false,
+                       CONTENT_MODEL_WIKITEXT,
+                       CONTENT_FORMAT_WIKITEXT,
+                       $schemaVersion
+               );
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
+
+               // FIXME: add multi-slot test case!
        }
 
-       function testFullStubPlain() {
+       /**
+        * @dataProvider schemaVersionProvider
+        */
+       function testFullStubPlain( $schemaVersion ) {
                // Preparing the dump
                $fname = $this->getNewTempFile();
 
                $dumper = $this->newDumpBackup(
-                       [ '--full', '--quiet', '--output', 'file:' . $fname, '--stub' ],
+                       [
+                               '--full',
+                               '--quiet',
+                               '--output',
+                               'file:' . $fname,
+                               '--stub',
+                               '--schema-version', $schemaVersion,
+                       ],
                        $this->pageId1,
                        $this->pageId4 + 1
                );
@@ -239,48 +307,98 @@ class BackupDumperPageTest extends DumpTestCase {
                $dumper->execute();
 
                // Checking the dumped data
-               $this->assertDumpStart( $fname );
+               $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+
+               $asserter->assertDumpStart( $fname );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
-                       $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
-               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
-                       $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
-               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
-                       $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_1,
+                       "BackupDumperTestP2Summary1",
+                       $this->textId2_1,
+                       23,
+                       "jprywrymfhysqllua29tj3sc7z39dl2"
+               );
+               $asserter->assertRevision(
+                       $this->revId2_2,
+                       "BackupDumperTestP2Summary2",
+                       $this->textId2_2,
+                       23,
+                       "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+                       false,
+                       $this->revId2_1
+               );
+               $asserter->assertRevision(
+                       $this->revId2_3,
+                       "BackupDumperTestP2Summary3",
+                       $this->textId2_3,
+                       23,
+                       "jfunqmh1ssfb8rs43r19w98k28gg56r",
+                       false,
+                       $this->revId2_2
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
                        $this->pageTitle4->getPrefixedText()
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
-                       $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
-               $this->assertPageEnd();
+               $asserter->assertRevision(
+                       $this->revId4_1,
+                       "Talk BackupDumperTestP1 Summary1",
+                       $this->textId4_1,
+                       35,
+                       "nktofwzd0tl192k3zfepmlzxoax1lpe"
+               );
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
-       function testCurrentStubPlain() {
+       /**
+        * @dataProvider schemaVersionProvider
+        */
+       function testCurrentStubPlain( $schemaVersion ) {
                // Preparing the dump
                $fname = $this->getNewTempFile();
 
                $dumper = $this->newDumpBackup(
-                       [ '--output', 'file:' . $fname ],
+                       [ '--output', 'file:' . $fname, '--schema-version', $schemaVersion ],
                        $this->pageId1,
                        $this->pageId4 + 1
                );
@@ -289,34 +407,62 @@ class BackupDumperPageTest extends DumpTestCase {
                $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB );
 
                // Checking the dumped data
-               $this->assertDumpStart( $fname );
+               $this->assertDumpSchema( $fname, $this->getXmlSchemaPath( $schemaVersion ) );
+
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+               $asserter->assertDumpStart( $fname );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
                        $this->pageTitle4->getPrefixedText()
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
-                       $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
-               $this->assertPageEnd();
+               $asserter->assertRevision(
+                       $this->revId4_1,
+                       "Talk BackupDumperTestP1 Summary1",
+                       $this->textId4_1,
+                       35,
+                       "nktofwzd0tl192k3zfepmlzxoax1lpe"
+               );
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
        function testCurrentStubGzip() {
@@ -336,34 +482,56 @@ class BackupDumperPageTest extends DumpTestCase {
 
                // Checking the dumped data
                $this->gunzip( $fname );
-               $this->assertDumpStart( $fname );
+
+               $asserter = $this->getDumpAsserter();
+               $asserter->assertDumpStart( $fname );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
                        $this->pageTitle4->getPrefixedText()
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+               $asserter->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
                        $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
-               $this->assertPageEnd();
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
        }
 
        /**
@@ -376,8 +544,10 @@ class BackupDumperPageTest extends DumpTestCase {
         *
         * We reproduce such a setup with our mini fixture, although we omit
         * chunks, and all the other gimmicks of xmldumps-backup.
+        *
+        * @dataProvider schemaVersionProvider
         */
-       function testXmlDumpsBackupUseCase() {
+       function testXmlDumpsBackupUseCase( $schemaVersion ) {
                $this->checkHasGzip();
 
                $fnameMetaHistory = $this->getNewTempFile();
@@ -389,7 +559,7 @@ class BackupDumperPageTest extends DumpTestCase {
                                "--output=gzip:" . $fnameMetaCurrent, "--filter=latest",
                                "--output=gzip:" . $fnameArticles, "--filter=latest",
                                "--filter=notalk", "--filter=namespace:!NS_USER",
-                               "--reporting=1000"
+                               "--reporting=1000", '--schema-version', $schemaVersion
                        ],
                        $this->pageId1,
                        $this->pageId4 + 1
@@ -413,89 +583,187 @@ class BackupDumperPageTest extends DumpTestCase {
                // Checking meta-history -------------------------------------------------
 
                $this->gunzip( $fnameMetaHistory );
-               $this->assertDumpStart( $fnameMetaHistory );
+               $this->assertDumpSchema( $fnameMetaHistory, $this->getXmlSchemaPath( $schemaVersion ) );
+
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+               $asserter->assertDumpStart( $fnameMetaHistory );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
-                       $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
-               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
-                       $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
-               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
-                       $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_1,
+                       "BackupDumperTestP2Summary1",
+                       $this->textId2_1,
+                       23,
+                       "jprywrymfhysqllua29tj3sc7z39dl2"
+               );
+               $asserter->assertRevision(
+                       $this->revId2_2,
+                       "BackupDumperTestP2Summary2",
+                       $this->textId2_2,
+                       23,
+                       "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+                       false,
+                       $this->revId2_1
+               );
+               $asserter->assertRevision(
+                       $this->revId2_3,
+                       "BackupDumperTestP2Summary3",
+                       $this->textId2_3,
+                       23,
+                       "jfunqmh1ssfb8rs43r19w98k28gg56r",
+                       false,
+                       $this->revId2_2
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
-                       $this->pageTitle4->getPrefixedText()
+                       $this->pageTitle4->getPrefixedText( $schemaVersion )
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
-                       $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
-               $this->assertPageEnd();
+               $asserter->assertRevision(
+                       $this->revId4_1,
+                       "Talk BackupDumperTestP1 Summary1",
+                       $this->textId4_1,
+                       35,
+                       "nktofwzd0tl192k3zfepmlzxoax1lpe"
+               );
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
 
                // Checking meta-current -------------------------------------------------
 
                $this->gunzip( $fnameMetaCurrent );
-               $this->assertDumpStart( $fnameMetaCurrent );
+               $this->assertDumpSchema( $fnameMetaCurrent, $this->getXmlSchemaPath( $schemaVersion ) );
+
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+               $asserter->assertDumpStart( $fnameMetaCurrent );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
 
                // Page 4
-               $this->assertPageStart(
+               $asserter->assertPageStart(
                        $this->pageId4,
                        $this->talk_namespace,
                        $this->pageTitle4->getPrefixedText()
                );
-               $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
-                       $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
-               $this->assertPageEnd();
+               $asserter->assertRevision(
+                       $this->revId4_1,
+                       "Talk BackupDumperTestP1 Summary1",
+                       $this->textId4_1,
+                       35,
+                       "nktofwzd0tl192k3zfepmlzxoax1lpe"
+               );
+               $asserter->assertPageEnd();
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
 
                // Checking articles -------------------------------------------------
 
                $this->gunzip( $fnameArticles );
-               $this->assertDumpStart( $fnameArticles );
+               $this->assertDumpSchema( $fnameArticles, $this->getXmlSchemaPath( $schemaVersion ) );
+
+               $asserter = $this->getDumpAsserter( $schemaVersion );
+               $asserter->assertDumpStart( $fnameArticles );
 
                // Page 1
-               $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
-               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
-                       $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId1,
+                       $this->namespace,
+                       $this->pageTitle1->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId1_1,
+                       "BackupDumperTestP1Summary1",
+                       $this->textId1_1,
+                       23,
+                       "0bolhl6ol7i6x0e7yq91gxgaan39j87"
+               );
+               $asserter->assertPageEnd();
 
                // Page 2
-               $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
-               $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
-                       $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
-               $this->assertPageEnd();
+               $asserter->assertPageStart(
+                       $this->pageId2,
+                       $this->namespace,
+                       $this->pageTitle2->getPrefixedText()
+               );
+               $asserter->assertRevision(
+                       $this->revId2_4,
+                       "BackupDumperTestP2Summary4 extra",
+                       $this->textId2_4,
+                       44,
+                       "6o1ciaxa6pybnqprmungwofc4lv00wv",
+                       false,
+                       $this->revId2_3
+               );
+               $asserter->assertPageEnd();
 
                // Page 3
                // -> Page is marked deleted. Hence not visible
@@ -503,7 +771,7 @@ class BackupDumperPageTest extends DumpTestCase {
                // Page 4
                // -> Page is not in $this->namespace. Hence not visible
 
-               $this->assertDumpEnd();
+               $asserter->assertDumpEnd();
 
                $this->expectETAOutput();
        }
diff --git a/tests/phpunit/maintenance/xml.xsd b/tests/phpunit/maintenance/xml.xsd
new file mode 100644 (file)
index 0000000..aea7d0d
--- /dev/null
@@ -0,0 +1,287 @@
+<?xml version='1.0'?>
+<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
+<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" 
+  xmlns:xs="http://www.w3.org/2001/XMLSchema" 
+  xmlns   ="http://www.w3.org/1999/xhtml"
+  xml:lang="en">
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+    <h1>About the XML namespace</h1>
+
+    <div class="bodytext">
+     <p>
+      This schema document describes the XML namespace, in a form
+      suitable for import by other schema documents.
+     </p>
+     <p>
+      See <a href="http://www.w3.org/XML/1998/namespace.html">
+      http://www.w3.org/XML/1998/namespace.html</a> and
+      <a href="http://www.w3.org/TR/REC-xml">
+      http://www.w3.org/TR/REC-xml</a> for information 
+      about this namespace.
+     </p>
+     <p>
+      Note that local names in this namespace are intended to be
+      defined only by the World Wide Web Consortium or its subgroups.
+      The names currently defined in this namespace are listed below.
+      They should not be used with conflicting semantics by any Working
+      Group, specification, or document instance.
+     </p>
+     <p>   
+      See further below in this document for more information about <a
+      href="#usage">how to refer to this schema document from your own
+      XSD schema documents</a> and about <a href="#nsversioning">the
+      namespace-versioning policy governing this schema document</a>.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:attribute name="lang">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>lang (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose value
+       is a language code for the natural language of the content of
+       any element; its value is inherited.  This name is reserved
+       by virtue of its definition in the XML specification.</p>
+     
+    </div>
+    <div>
+     <h4>Notes</h4>
+     <p>
+      Attempting to install the relevant ISO 2- and 3-letter
+      codes as the enumerated possible values is probably never
+      going to be a realistic possibility.  
+     </p>
+     <p>
+      See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
+       http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
+      and the IANA language subtag registry at
+      <a href="http://www.iana.org/assignments/language-subtag-registry">
+       http://www.iana.org/assignments/language-subtag-registry</a>
+      for further information.
+     </p>
+     <p>
+      The union allows for the 'un-declaration' of xml:lang with
+      the empty string.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:union memberTypes="xs:language">
+    <xs:simpleType>    
+     <xs:restriction base="xs:string">
+      <xs:enumeration value=""/>
+     </xs:restriction>
+    </xs:simpleType>
+   </xs:union>
+  </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="space">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>space (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose
+       value is a keyword indicating what whitespace processing
+       discipline is intended for the content of the element; its
+       value is inherited.  This name is reserved by virtue of its
+       definition in the XML specification.</p>
+     
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+  <xs:simpleType>
+   <xs:restriction base="xs:NCName">
+    <xs:enumeration value="default"/>
+    <xs:enumeration value="preserve"/>
+   </xs:restriction>
+  </xs:simpleType>
+ </xs:attribute>
+ <xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>base (as an attribute name)</h3>
+      <p>
+       denotes an attribute whose value
+       provides a URI to be used as the base for interpreting any
+       relative URIs in the scope of the element on which it
+       appears; its value is inherited.  This name is reserved
+       by virtue of its definition in the XML Base specification.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="id" type="xs:ID">
+  <xs:annotation>
+   <xs:documentation>
+    <div>
+     
+      <h3>id (as an attribute name)</h3> 
+      <p>
+       denotes an attribute whose value
+       should be interpreted as if declared to be of type ID.
+       This name is reserved by virtue of its definition in the
+       xml:id specification.</p>
+     
+     <p>
+      See <a
+      href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
+      for information about this attribute.
+     </p>
+    </div>
+   </xs:documentation>
+  </xs:annotation>
+ </xs:attribute>
+
+ <xs:attributeGroup name="specialAttrs">
+  <xs:attribute ref="xml:base"/>
+  <xs:attribute ref="xml:lang"/>
+  <xs:attribute ref="xml:space"/>
+  <xs:attribute ref="xml:id"/>
+ </xs:attributeGroup>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div>
+   
+    <h3>Father (in any context at all)</h3> 
+
+    <div class="bodytext">
+     <p>
+      denotes Jon Bosak, the chair of 
+      the original XML Working Group.  This name is reserved by 
+      the following decision of the W3C XML Plenary and 
+      XML Coordination groups:
+     </p>
+     <blockquote>
+       <p>
+       In appreciation for his vision, leadership and
+       dedication the W3C XML Plenary on this 10th day of
+       February, 2000, reserves for Jon Bosak in perpetuity
+       the XML name "xml:Father".
+       </p>
+     </blockquote>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div xml:id="usage" id="usage">
+    <h2><a name="usage">About this schema document</a></h2>
+
+    <div class="bodytext">
+     <p>
+      This schema defines attributes and an attribute group suitable
+      for use by schemas wishing to allow <code>xml:base</code>,
+      <code>xml:lang</code>, <code>xml:space</code> or
+      <code>xml:id</code> attributes on elements they define.
+     </p>
+     <p>
+      To enable this, such a schema must import this schema for
+      the XML namespace, e.g. as follows:
+     </p>
+     <pre>
+          &lt;schema . . .>
+           . . .
+           &lt;import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     </pre>
+     <p>
+      or
+     </p>
+     <pre>
+           &lt;import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     </pre>
+     <p>
+      Subsequently, qualified reference to any of the attributes or the
+      group defined below will have the desired effect, e.g.
+     </p>
+     <pre>
+          &lt;type . . .>
+           . . .
+           &lt;attributeGroup ref="xml:specialAttrs"/>
+     </pre>
+     <p>
+      will define a type which will schema-validate an instance element
+      with any of those attributes.
+     </p>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+  <xs:documentation>
+   <div id="nsversioning" xml:id="nsversioning">
+    <h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
+    <div class="bodytext">
+     <p>
+      In keeping with the XML Schema WG's standard versioning
+      policy, this schema document will persist at
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd</a>.
+     </p>
+     <p>
+      At the date of issue it can also be found at
+      <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd</a>.
+     </p>
+     <p>
+      The schema document at that URI may however change in the future,
+      in order to remain compatible with the latest version of XML
+      Schema itself, or with the XML namespace itself.  In other words,
+      if the XML Schema or XML namespaces change, the version of this
+      document at <a href="http://www.w3.org/2001/xml.xsd">
+       http://www.w3.org/2001/xml.xsd 
+      </a> 
+      will change accordingly; the version at 
+      <a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd 
+      </a> 
+      will not change.
+     </p>
+     <p>
+      Previous dated (and unchanging) versions of this schema 
+      document are at:
+     </p>
+     <ul>
+      <li><a href="http://www.w3.org/2009/01/xml.xsd">
+       http://www.w3.org/2009/01/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2007/08/xml.xsd">
+       http://www.w3.org/2007/08/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2004/10/xml.xsd">
+       http://www.w3.org/2004/10/xml.xsd</a></li>
+      <li><a href="http://www.w3.org/2001/03/xml.xsd">
+       http://www.w3.org/2001/03/xml.xsd</a></li>
+     </ul>
+    </div>
+   </div>
+  </xs:documentation>
+ </xs:annotation>
+
+</xs:schema>
+