phpunit: Repair GLOBALS reset in MediaWikiUnitTestCase
authorTimo Tijhof <krinklemail@gmail.com>
Wed, 7 Aug 2019 13:40:55 +0000 (14:40 +0100)
committerTimo Tijhof <krinklemail@gmail.com>
Mon, 2 Sep 2019 19:58:34 +0000 (20:58 +0100)
This code didn't work because the $GLOBALS array is exposed by reference.
Once this reference was broken by unset(), the rest just manipulated a
local array that happens to be called "GLOBALS". It must not be unset or
re-assigned. It can only be changed in-place.

Before this, the execution of a MediaWikiUnitTestCase test stored a
copy of GLOBALS in unitGlobals, then lost the GLOBALS pointer and
created a new variable called "GLOBALS". As such, the tearDown() function
didn't do what it meant to do, either – which then results in odd
failures like T230023

Rewrite it as follows:

* In setup, store the current GLOBALS keys and values, then reduce
  GLOBALS to only the whitelisted keys and values.

* In teardown, restore the original state.

* As optimisation, do this from setUpBeforeClass as well, so that
  there are relatively few globals to reset between tests.
  (Thanks @Simetrical!)

The following tests were previously passing by accident under
MediaWikiUnitTestCase but actually did depend on global config.

* MainSlotRoleHandlerTest (…, ContentHandler, $wgContentHandlers)
* SlotRecordTest (…, ContentHandler, $wgContentHandlers)
* WikiReferenceTest (wfParseUrl, $wgUrlProtocols)
* DifferenceEngineSlotDiffRendererTest (DifferenceEngine, wfDebug, …)
* SlotDiffRendererTest (…, ContentHandler, $wgContentHandlers)
* FileBackendDBRepoWrapperTest (wfWikiID, "Backend domain ID not provided")
* JpegMetadataExtractorTest (…, wfDebug, …, LoggerFactory, …)
* ParserFactoryTest (…, wfDebug, …, LoggerFactory, InvalidArgumentException)
* MediaWikiPageNameNormalizerTest (…, wfDebug, …, LoggerFactory, …)
* SiteExporterTest (SiteImporter, wfLogWarning, …)
* SiteImporterTest (Site::newForType, $wgSiteTypes)
* ZipDirectoryReaderTest (…, wfDebug, …, LoggerFactory, …)

Bug: T230023
Change-Id: Ic22075bb5e81b7c2c4c1b8647547aa55306a10a7

30 files changed:
tests/common/TestSetup.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/bootstrap.php
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/WikiReferenceTest.php [new file with mode: 0644]
tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/includes/diff/SlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php [new file with mode: 0644]
tests/phpunit/includes/media/JpegMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/ParserFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteExporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.xml [new file with mode: 0644]
tests/phpunit/includes/utils/ZipDirectoryReaderTest.php [new file with mode: 0644]
tests/phpunit/phpunit.php
tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php [deleted file]
tests/phpunit/unit/includes/Revision/SlotRecordTest.php [deleted file]
tests/phpunit/unit/includes/WikiReferenceTest.php [deleted file]
tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [deleted file]
tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php [deleted file]
tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php [deleted file]
tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php [deleted file]
tests/phpunit/unit/includes/parser/ParserFactoryTest.php [deleted file]
tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteExporterTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteImporterTest.php [deleted file]
tests/phpunit/unit/includes/site/SiteImporterTest.xml [deleted file]
tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php [deleted file]

index d4df8ae..141e307 100644 (file)
@@ -4,6 +4,23 @@
  * Common code for test environment initialisation and teardown
  */
 class TestSetup {
+       public static $bootstrapGlobals;
+
+       /**
+        * For use in MediaWikiUnitTestCase.
+        *
+        * This should be called before DefaultSettings.php or Setup.php loads.
+        */
+       public static function snapshotGlobals() {
+               self::$bootstrapGlobals = [];
+               foreach ( $GLOBALS as $key => $_ ) {
+                       // Support: HHVM (avoid self-ref)
+                       if ( $key !== 'GLOBALS' ) {
+                               self::$bootstrapGlobals[ $key ] =& $GLOBALS[$key];
+                       }
+               }
+       }
+
        /**
         * This should be called before Setup.php, e.g. from the finalSetup() method
         * of a Maintenance subclass
index bb018aa..3f876ae 100644 (file)
@@ -34,30 +34,89 @@ abstract class MediaWikiUnitTestCase extends TestCase {
        use MediaWikiCoversValidator;
        use MediaWikiTestCaseTrait;
 
-       private $unitGlobals = [];
+       private static $originalGlobals;
+       private static $unitGlobals;
 
-       protected function setUp() {
-               parent::setUp();
-               $reflection = new ReflectionClass( $this );
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               $reflection = new ReflectionClass( static::class );
                $dirSeparator = DIRECTORY_SEPARATOR;
                if ( stripos( $reflection->getFilename(), "${dirSeparator}unit${dirSeparator}" ) === false ) {
-                       $this->fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
+                       self::fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
+               }
+
+               if ( defined( 'HHVM_VERSION' ) ) {
+                       // There are a number of issues we encountered in trying to make this
+                       // work on HHVM. Specifically, once an MediaWikiIntegrationTestCase executes
+                       // before us, the original globals go missing. This might have to do with
+                       // one of the non-unit tests passing GLOBALS somewhere and causing HHVM
+                       // to get confused somehow.
+                       return;
+               }
+
+               self::$unitGlobals =& TestSetup::$bootstrapGlobals;
+               // The autoloader may change between bootstrap and the first test,
+               // so (lazily) capture these here instead.
+               self::$unitGlobals['wgAutoloadClasses'] =& $GLOBALS['wgAutoloadClasses'];
+               self::$unitGlobals['wgAutoloadLocalClasses'] =& $GLOBALS['wgAutoloadLocalClasses'];
+               // This value should always be true.
+               self::$unitGlobals['wgAutoloadAttemptLowercase'] = true;
+
+               // Would be nice if we coud simply replace $GLOBALS as a whole,
+               // but unsetting or re-assigning that breaks the reference of this magic
+               // variable. Thus we have to modify it in place.
+               self::$originalGlobals = [];
+               foreach ( $GLOBALS as $key => $_ ) {
+                       // Stash current values
+                       self::$originalGlobals[$key] =& $GLOBALS[$key];
+
+                       // Remove globals not part of the snapshot (see bootstrap.php, phpunit.php).
+                       // Support: HHVM (avoid self-ref)
+                       if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
+                               unset( $GLOBALS[$key] );
+                       }
                }
-               $this->unitGlobals = $GLOBALS;
-               unset( $GLOBALS );
-               $GLOBALS = [];
-               // Add back the minimal set of globals needed for unit tests to run for core +
-               // extensions/skins.
-               foreach ( $this->unitGlobals['wgPhpUnitBootstrapGlobals'] ?? [] as $key => $value ) {
-                       $GLOBALS[ $key ] = $this->unitGlobals[ $key ];
+               // Restore values from the early snapshot
+               // Not by ref because tests must not be able to modify the snapshot.
+               foreach ( self::$unitGlobals as $key => $value ) {
+                       $GLOBALS[ $key ] = $value;
                }
        }
 
        protected function tearDown() {
-               $GLOBALS = $this->unitGlobals;
+               if ( !defined( 'HHVM_VERSION' ) ) {
+                       // Quick reset between tests
+                       foreach ( $GLOBALS as $key => $_ ) {
+                               if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
+                                       unset( $GLOBALS[$key] );
+                               }
+                       }
+                       foreach ( self::$unitGlobals as $key => $value ) {
+                               $GLOBALS[ $key ] = $value;
+                       }
+               }
+
                parent::tearDown();
        }
 
+       public static function tearDownAfterClass() {
+               if ( !defined( 'HHVM_VERSION' ) ) {
+                       // Remove globals created by the test
+                       foreach ( $GLOBALS as $key => $_ ) {
+                               if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$originalGlobals ) ) {
+                                       unset( $GLOBALS[$key] );
+                               }
+                       }
+                       // Restore values (including reference!)
+                       foreach ( self::$originalGlobals as $key => &$value ) {
+                               $GLOBALS[ $key ] =& $value;
+                       }
+               }
+
+               parent::tearDownAfterClass();
+       }
+
        /**
         * Create a temporary hook handler which will be reset by tearDown.
         * This replaces other handlers for the same hook.
index 9e79496..477dbd2 100644 (file)
@@ -55,19 +55,15 @@ define( 'MW_CONFIG_FILE', "$IP/LocalSettings.php" );
 
 // these variables must be defined before setup runs
 $GLOBALS['IP'] = $IP;
-// Set bootstrap globals to reuse in MediaWikiUnitTestCase
-$bootstrapGlobals = [];
-foreach ( $GLOBALS as $key => $value ) {
-       $bootstrapGlobals[ $key ] = $value;
-}
-$GLOBALS['wgPhpUnitBootstrapGlobals'] = $bootstrapGlobals;
-// Faking for Setup.php
+
+require_once "$IP/tests/common/TestSetup.php";
+TestSetup::snapshotGlobals();
+
+// Faking in lieu of Setup.php
 $GLOBALS['wgScopeTest'] = 'MediaWiki Setup.php scope test';
 $GLOBALS['wgCommandLineMode'] = true;
 $GLOBALS['wgAutoloadClasses'] = [];
 
-require_once "$IP/tests/common/TestSetup.php";
-
 wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/tests/common/TestsAutoLoader.php" );
 wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..60d456d
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\MainSlotRoleHandler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\MainSlotRoleHandler
+ */
+class MainSlotRoleHandlerTest extends \MediaWikiIntegrationTestCase {
+
+       private function makeTitleObject( $ns ) {
+               /** @var Title|MockObject $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $title->method( 'getNamespace' )
+                       ->willReturn( $ns );
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new MainSlotRoleHandler( [] );
+               $this->assertSame( 'main', $handler->getRole() );
+               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
+        */
+       public function testFetDefaultModel() {
+               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
+
+               // For the main handler, the namespace determins the default model
+               $titleMain = $this->makeTitleObject( NS_MAIN );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
+
+               $title100 = $this->makeTitleObject( 100 );
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               // For the main handler, (nearly) all models are allowed
+               $title = $this->makeTitleObject( NS_MAIN );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               $this->assertTrue( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php
new file mode 100644 (file)
index 0000000..7ffe004
--- /dev/null
@@ -0,0 +1,415 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends \MediaWikiIntegrationTestCase {
+
+       private function makeRow( $data = [] ) {
+               $data = $data + [
+                       'slot_id' => 1234,
+                       'slot_content_id' => 33,
+                       'content_size' => '5',
+                       'content_sha1' => 'someHash',
+                       'content_address' => 'tt:456',
+                       'model_name' => CONTENT_MODEL_WIKITEXT,
+                       'format_name' => CONTENT_FORMAT_WIKITEXT,
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '1',
+                       'role_name' => 'myRole',
+               ];
+               return (object)$data;
+       }
+
+       public function testCompleteConstruction() {
+               $row = $this->makeRow();
+               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertTrue( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 5, $record->getSize() );
+               $this->assertSame( 'someHash', $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 1, $record->getOrigin() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( 33, $record->getContentId() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testConstructionDeferred() {
+               $row = $this->makeRow( [
+                       'content_size' => null, // to be computed
+                       'content_sha1' => null, // to be computed
+                       'format_name' => function () {
+                               return CONTENT_FORMAT_WIKITEXT;
+                       },
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
+               ] );
+
+               $content = function () {
+                       return new WikitextContent( 'A' );
+               };
+
+               $record = new SlotRecord( $row, $content );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testNewUnsaved() {
+               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+               $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->hasRevision() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotEmpty( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function provideInvalidConstruction() {
+               yield 'both null' => [ null, null ];
+               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+               yield 'null content' => [ (object)[], null ];
+       }
+
+       /**
+        * @dataProvider provideInvalidConstruction
+        */
+       public function testInvalidConstruction( $row, $content ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new SlotRecord( $row, $content );
+       }
+
+       public function testGetContentId_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getContentId();
+       }
+
+       public function testGetAddress_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getAddress();
+       }
+
+       public function provideIncomplete() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               yield 'unsaved' => [ $unsaved ];
+
+               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $inherited = SlotRecord::newInherited( $parent );
+               yield 'inherited' => [ $inherited ];
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetRevision_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getRevision();
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetOrigin_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getOrigin();
+       }
+
+       public function provideHashStability() {
+               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+       }
+
+       /**
+        * @dataProvider provideHashStability
+        */
+       public function testHashStability( $text, $hash ) {
+               // Changing the output of the hash function will break things horribly!
+
+               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+               $this->assertSame( $hash, $record->getSha1() );
+       }
+
+       public function testHashComputed() {
+               $row = $this->makeRow();
+               $row->content_sha1 = '';
+
+               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
+               $this->assertNotEmpty( $rec->getSha1() );
+       }
+
+       public function testNewWithSuppressedContent() {
+               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $output = SlotRecord::newWithSuppressedContent( $input );
+
+               $this->setExpectedException( SuppressedDataException::class );
+               $output->getContent();
+       }
+
+       public function testNewInherited() {
+               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, before saving revision meta-data.
+               $inherited = SlotRecord::newInherited( $parent );
+
+               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+               $this->assertSame( $parent->getContent(), $inherited->getContent() );
+               $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
+               $this->assertFalse( $inherited->hasRevision() );
+
+               // make sure we didn't mess with the internal state of $parent
+               $this->assertFalse( $parent->isInherited() );
+               $this->assertSame( 7, $parent->getRevision() );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved(
+                       10,
+                       $inherited->getContentId(),
+                       $inherited->getAddress(),
+                       $inherited
+               );
+               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+               $this->assertSame( $parent->getContent(), $saved->getContent() );
+               $this->assertTrue( $saved->isInherited() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertSame( 10, $saved->getRevision() );
+
+               // make sure we didn't mess with the internal state of $parent or $inherited
+               $this->assertSame( 7, $parent->getRevision() );
+               $this->assertFalse( $inherited->hasRevision() );
+       }
+
+       public function testNewSaved() {
+               // This would happen while doing an edit, before saving revision meta-data.
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+               $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
+               $this->assertSame( 'theNewAddress', $saved->getAddress() );
+               $this->assertSame( 20, $saved->getContentId() );
+               $this->assertSame( 'A', $saved->getContent()->getText() );
+               $this->assertSame( 10, $saved->getRevision() );
+               $this->assertSame( 10, $saved->getOrigin() );
+
+               // make sure we didn't mess with the internal state of $unsaved
+               $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
+               $this->assertFalse( $unsaved->hasRevision() );
+       }
+
+       public function provideNewSaved_LogicException() {
+               $freshRow = $this->makeRow( [
+                       'content_id' => 10,
+                       'content_address' => 'address:1',
+                       'slot_origin' => 1,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+               $inheritedRow = $this->makeRow( [
+                       'content_id' => null,
+                       'content_address' => null,
+                       'slot_origin' => 0,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_LogicException
+        */
+       public function testNewSaved_LogicException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( LogicException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideNewSaved_InvalidArgumentException() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_InvalidArgumentException
+        */
+       public function testNewSaved_InvalidArgumentException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/WikiReferenceTest.php b/tests/phpunit/includes/WikiReferenceTest.php
new file mode 100644 (file)
index 0000000..702d3d7
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * @covers WikiReference
+ */
+class WikiReferenceTest extends MediaWikiIntegrationTestCase {
+
+       public function provideGetDisplayName() {
+               return [
+                       'http' => [ 'foo.bar', 'http://foo.bar' ],
+                       'https' => [ 'foo.bar', 'http://foo.bar' ],
+
+                       // apparently, this is the expected behavior
+                       'invalid' => [ 'purple kittens', 'purple kittens' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetDisplayName
+        */
+       public function testGetDisplayName( $expected, $canonicalServer ) {
+               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
+               $this->assertEquals( $expected, $reference->getDisplayName() );
+       }
+
+       public function testGetCanonicalServer() {
+               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
+               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
+       }
+
+       public function provideGetCanonicalUrl() {
+               return [
+                       'no fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               'https://acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               'https://acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               'https://acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               'https://acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        */
+       public function testGetCanonicalUrl(
+               $expected, $canonicalServer, $server, $path, $page, $fragmentId
+       ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        * @note getUrl is an alias for getCanonicalUrl
+        */
+       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
+       }
+
+       public function provideGetFullUrl() {
+               return [
+                       'no fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               '//acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               '//acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               '//acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               '//acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFullUrl
+        */
+       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..e5c23d1
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers DifferenceEngineSlotDiffRenderer
+ */
+class DifferenceEngineSlotDiffRendererTest extends MediaWikiIntegrationTestCase {
+
+       public function testGetDiff() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
+               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
+               $this->assertEquals( 'xxx|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( null, $newContent );
+               $this->assertEquals( '|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
+               $this->assertEquals( 'xxx|', $diff );
+       }
+
+       public function testAddModules() {
+               $output = $this->getMockBuilder( OutputPage::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'addModules' ] )
+                       ->getMock();
+               $output->expects( $this->once() )
+                       ->method( 'addModules' )
+                       ->with( 'foo' );
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $slotDiffRenderer->addModules( $output );
+       }
+
+       public function testGetExtraCacheKeys() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
+               $this->assertSame( [ 'foo' ], $extraCacheKeys );
+       }
+
+}
diff --git a/tests/phpunit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/includes/diff/SlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..9f15517
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+use Wikimedia\Assert\ParameterTypeException;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers SlotDiffRenderer
+ */
+class SlotDiffRendererTest extends \MediaWikiIntegrationTestCase {
+
+       /**
+        * @dataProvider provideNormalizeContents
+        */
+       public function testNormalizeContents(
+               $oldContent, $newContent, $allowedClasses,
+               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
+       ) {
+               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
+                       ->getMock();
+               try {
+                       // __call needs help deciding which parameter to take by reference
+                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
+                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
+                       $this->assertEquals( $expectedOldContent, $oldContent );
+                       $this->assertEquals( $expectedNewContent, $newContent );
+               } catch ( Exception $e ) {
+                       if ( !$expectedExceptionClass ) {
+                               throw $e;
+                       }
+                       $this->assertInstanceOf( $expectedExceptionClass, $e );
+               }
+       }
+
+       public function provideNormalizeContents() {
+               return [
+                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
+                       'left null' => [
+                               null, new WikitextContent( 'abc' ), null,
+                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
+                       ],
+                       'right null' => [
+                               new WikitextContent( 'def' ), null, null,
+                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (subclass)' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (null)' => [
+                               new WikitextContent( 'abc' ), null, TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter failure (left)' => [
+                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter failure (right)' => [
+                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter (array syntax)' => [
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
+                       ],
+                       'type filter failure (array syntax)' => [
+                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               null, null, ParameterTypeException::class,
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php
new file mode 100644 (file)
index 0000000..69fc367
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class FileBackendDBRepoWrapperTest extends MediaWikiIntegrationTestCase {
+       protected $backendName = 'foo-backend';
+       protected $repoName = 'pureTestRepo';
+
+       /**
+        * @dataProvider getBackendPathsProvider
+        * @covers FileBackendDBRepoWrapper::getBackendPaths
+        */
+       public function testGetBackendPaths(
+               $mocks,
+               $latest,
+               $dbReadsExpected,
+               $dbReturnValue,
+               $originalPath,
+               $expectedBackendPath,
+               $message ) {
+               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
+
+               $dbMock->expects( $dbReadsExpected )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( $dbReturnValue ) );
+
+               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
+
+               $this->assertEquals(
+                       $expectedBackendPath,
+                       $newPaths[0],
+                       $message );
+       }
+
+       public function getBackendPathsProvider() {
+               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
+               $mocksForCaching = $this->getMocks();
+
+               return [
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Public path translated correctly',
+                       ],
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'LRU cache leveraged',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Latest obtained',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-deleted/f/o/foobar.jpg',
+                               $prefix . '-original/f/o/o/foobar',
+                               'Deleted path translated correctly',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               null,
+                               $prefix . '-public/b/a/baz.jpg',
+                               $prefix . '-public/b/a/baz.jpg',
+                               'Path left untouched if no sha1 can be found',
+                       ],
+               ];
+       }
+
+       /**
+        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
+        */
+       public function testGetFileContentsMulti() {
+               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
+
+               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
+               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-public/f/o/foobar.jpg';
+
+               $dbMock->expects( $this->once() )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
+
+               $backendMock->expects( $this->once() )
+                       ->method( 'getFileContentsMulti' )
+                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
+
+               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
+
+               $this->assertEquals(
+                       [ $filenamePath => 'foo' ],
+                       $result,
+                       'File contents paths translated properly'
+               );
+       }
+
+       protected function getMocks() {
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
+                       ->disableOriginalClone()
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $backendMock = $this->getMockBuilder( FSFileBackend::class )
+                       ->setConstructorArgs( [ [
+                                       'name' => $this->backendName,
+                                       'wikiId' => wfWikiID()
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
+                       ->setMethods( [ 'getDB' ] )
+                       ->setConstructorArgs( [ [
+                                       'backend' => $backendMock,
+                                       'repoName' => $this->repoName,
+                                       'dbHandleFactory' => null
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
+
+               return [ $dbMock, $backendMock, $wrapperMock ];
+       }
+}
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..6c56510
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ *
+ * @group Media
+ * @covers JpegMetadataExtractor
+ */
+class JpegMetadataExtractorTest extends MediaWikiIntegrationTestCase {
+
+       protected $filePath;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->filePath = __DIR__ . '/../../data/media/';
+       }
+
+       /**
+        * We also use this test to test padding bytes don't
+        * screw stuff up
+        *
+        * @param string $file Filename
+        *
+        * @dataProvider provideUtf8Comment
+        */
+       public function testUtf8Comment( $file ) {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
+       }
+
+       public static function provideUtf8Comment() {
+               return [
+                       [ 'jpeg-comment-utf.jpg' ],
+                       [ 'jpeg-padding-even.jpg' ],
+                       [ 'jpeg-padding-odd.jpg' ],
+               ];
+       }
+
+       /** The file is iso-8859-1, but it should get auto converted */
+       public function testIso88591Comment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
+       }
+
+       /** Comment values that are non-textual (random binary junk) should not be shown.
+        * The example test file has a comment with a 0x5 byte in it which is a control character
+        * and considered binary junk for our purposes.
+        */
+       public function testBinaryCommentStripped() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+               $this->assertEmpty( $res['COM'] );
+       }
+
+       /* Very rarely a file can have multiple comments.
+        *   Order of comments is based on order inside the file.
+        */
+       public function testMultipleComment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
+       }
+
+       public function testXMPExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testPSIRExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = '50686f746f73686f7020332e30003842494d04040000000'
+                       . '000181c02190004746573741c02190003666f6f1c020000020004';
+               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+       }
+
+       public function testXMPExtractionAltAppId() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testIPTCHashComparisionNoHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-no-hash', $res );
+       }
+
+       public function testIPTCHashComparisionBadHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-bad-hash', $res );
+       }
+
+       public function testIPTCHashComparisionGoodHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-good-hash', $res );
+       }
+
+       public function testExifByteOrder() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+               $expected = 'BE';
+               $this->assertEquals( $expected, $res['byteOrder'] );
+       }
+
+       public function testInfiniteRead() {
+               // test file truncated right after a segment, which previously
+               // caused an infinite loop looking for the next segment byte.
+               // Should get past infinite loop and throw in wfUnpack()
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
+       }
+
+       public function testInfiniteRead2() {
+               // test file truncated after a segment's marker and size, which
+               // would cause a seek past end of file. Seek past end of file
+               // doesn't actually fail, but prevents further reading and was
+               // devolving into the previous case (testInfiniteRead).
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
+       }
+}
diff --git a/tests/phpunit/includes/parser/ParserFactoryTest.php b/tests/phpunit/includes/parser/ParserFactoryTest.php
new file mode 100644 (file)
index 0000000..e6e9db4
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @covers ParserFactory
+ */
+class ParserFactoryTest extends MediaWikiIntegrationTestCase {
+       use FactoryArgTestTrait;
+
+       protected static function getFactoryClass() {
+               return ParserFactory::class;
+       }
+
+       protected static function getInstanceClass() {
+               return Parser::class;
+       }
+
+       protected static function getFactoryMethodName() {
+               return 'create';
+       }
+
+       protected static function getExtraClassArgCount() {
+               // The parser factory itself is passed to the parser
+               return 1;
+       }
+
+       protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
+               if ( $param->getPosition() === 0 ) {
+                       return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ];
+               }
+               return [];
+       }
+}
diff --git a/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php
new file mode 100644 (file)
index 0000000..7a6647b
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+use MediaWiki\Site\MediaWikiPageNameNormalizer;
+
+/**
+ * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.27
+ *
+ * @group Site
+ * @group medium
+ *
+ * @author Marius Hoch
+ */
+class MediaWikiPageNameNormalizerTest extends MediaWikiIntegrationTestCase {
+
+       /**
+        * @dataProvider normalizePageTitleProvider
+        */
+       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
+               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
+
+               $normalizer = new MediaWikiPageNameNormalizer(
+                       new MediaWikiPageNameNormalizerTestMockHttp()
+               );
+
+               $this->assertSame(
+                       $expected,
+                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
+               );
+       }
+
+       public function normalizePageTitleProvider() {
+               // Response are taken from wikidata and kkwiki using the following API request
+               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
+               return [
+                       'universe (Q1)' => [
+                               'Q1',
+                               'Q1',
+                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
+                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
+                       ],
+                       'Q404 redirects to Q395' => [
+                               'Q395',
+                               'Q404',
+                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
+                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
+                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
+                       ],
+                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
+                               'Д',
+                               'D',
+                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
+                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
+                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
+                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
+                               . '"lastrevid":2373618,"length":3501}}}}'
+                       ],
+                       'there is no Q0' => [
+                               false,
+                               'Q0',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
+                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
+                       ],
+                       'invalid title' => [
+                               false,
+                               '{{',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
+                               . '"invalidreason":"The requested page title contains invalid '
+                               . 'characters: \"{\".","invalid":""}}}}'
+                       ],
+                       'error on get' => [ false, 'ABC', false ]
+               ];
+       }
+
+}
+
+/**
+ * @private
+ * @see Http
+ */
+class MediaWikiPageNameNormalizerTestMockHttp extends Http {
+
+       /**
+        * @var mixed
+        */
+       public static $response;
+
+       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
+
+               return self::$response;
+       }
+}
diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php
new file mode 100644 (file)
index 0000000..158be69
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteExporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteExporterTest extends MediaWikiIntegrationTestCase {
+
+       public function testConstructor_InvalidArgument() {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new SiteExporter( 'Foo' );
+       }
+
+       public function testExportSites() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( [ $foo, $acme ] );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $this->assertContains( '<sites ', $xml );
+               $this->assertContains( '<site>', $xml );
+               $this->assertContains( '<globalid>Foo</globalid>', $xml );
+               $this->assertContains( '</site>', $xml );
+               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
+               $this->assertContains( '<group>Test</group>', $xml );
+               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
+               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
+               $this->assertContains( '</sites>', $xml );
+
+               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
+               $xsdFile = __DIR__ . '/../../../../docs/sitelist-1.0.xsd';
+               $xsdData = file_get_contents( $xsdFile );
+
+               $document = new DOMDocument();
+               $document->loadXML( $xml, LIBXML_NONET );
+               $document->schemaValidateSource( $xsdData );
+       }
+
+       private function newSiteStore( SiteList $sites ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
+                               foreach ( $moreSites as $site ) {
+                                       $sites->setSite( $site );
+                               }
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               return $store;
+       }
+
+       public function provideRoundTrip() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               new SiteList()
+                       ],
+
+                       'some' => [
+                               new SiteList( [ $foo, $acme, $dewiki ] ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideRoundTrip()
+        */
+       public function testRoundTrip( SiteList $sites ) {
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( $sites );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $actualSites = new SiteList();
+               $store = $this->newSiteStore( $actualSites );
+
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( $xml );
+
+               $this->assertEquals( $sites, $actualSites );
+       }
+
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.php b/tests/phpunit/includes/site/SiteImporterTest.php
new file mode 100644 (file)
index 0000000..c614dd4
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteImporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteImporterTest extends MediaWikiIntegrationTestCase {
+
+       private function newSiteImporter( array $expectedSites, $errorCount ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
+                               $this->assertSitesEqual( $expectedSites, $sites );
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
+               $errorHandler->expects( $this->exactly( $errorCount ) )
+                       ->method( 'error' );
+
+               $importer = new SiteImporter( $store );
+               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
+
+               return $importer;
+       }
+
+       public function assertSitesEqual( $expected, $actual, $message = '' ) {
+               $this->assertEquals(
+                       $this->getSerializedSiteList( $expected ),
+                       $this->getSerializedSiteList( $actual ),
+                       $message
+               );
+       }
+
+       public function provideImportFromXML() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               '<sites></sites>',
+                               [],
+                       ],
+                       'no sites' => [
+                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
+                               [],
+                       ],
+                       'minimal' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                               '</sites>',
+                               [ $foo ],
+                       ],
+                       'full' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                                       '<site type="mediawiki">' .
+                                               '<source>meta.wikimedia.org</source>' .
+                                               '<globalid>dewiki</globalid>' .
+                                               '<localid type="interwiki">wikipedia</localid>' .
+                                               '<localid type="equivalent">de</localid>' .
+                                               '<group>wikipedia</group>' .
+                                               '<forward/>' .
+                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
+                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme, $dewiki ],
+                       ],
+                       'skip' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site><barf>Foo</barf></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<silly>boop!</silly>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme ],
+                               1
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideImportFromXML
+        */
+       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
+               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
+               $importer->importFromXML( $xml );
+       }
+
+       public function testImportFromXML_malformed() {
+               $this->setExpectedException( Exception::class );
+
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( 'THIS IS NOT XML' );
+       }
+
+       public function testImportFromFile() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
+
+               $file = __DIR__ . '/SiteImporterTest.xml';
+               $importer->importFromFile( $file );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @return array[]
+        */
+       private function getSerializedSiteList( $sites ) {
+               $serialized = [];
+
+               foreach ( $sites as $site ) {
+                       $key = $site->getGlobalId();
+                       $data = unserialize( $site->serialize() );
+
+                       $serialized[$key] = $data;
+               }
+
+               return $serialized;
+       }
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.xml b/tests/phpunit/includes/site/SiteImporterTest.xml
new file mode 100644 (file)
index 0000000..720b1fa
--- /dev/null
@@ -0,0 +1,19 @@
+<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
+       <site><globalid>Foo</globalid></site>
+       <site>
+               <globalid>acme.com</globalid>
+               <localid type="interwiki">acme</localid>
+               <group>Test</group>
+               <path type="link">http://acme.com/</path>
+       </site>
+       <site type="mediawiki">
+               <source>meta.wikimedia.org</source>
+               <globalid>dewiki</globalid>
+               <localid type="interwiki">wikipedia</localid>
+               <localid type="equivalent">de</localid>
+               <group>wikipedia</group>
+               <forward/>
+               <path type="link">http://de.wikipedia.org/w/</path>
+               <path type="page_path">http://de.wikipedia.org/wiki/</path>
+       </site>
+</sites>
diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
new file mode 100644 (file)
index 0000000..492b250
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ */
+class ZipDirectoryReaderTest extends MediaWikiIntegrationTestCase {
+
+       protected $zipDir;
+       protected $entries;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->zipDir = __DIR__ . '/../../data/zip';
+       }
+
+       function zipCallback( $entry ) {
+               $this->entries[] = $entry;
+       }
+
+       function readZipAssertError( $file, $error, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+       }
+
+       function readZipAssertSuccess( $file, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->isOK(), $assertMessage );
+       }
+
+       public function testEmpty() {
+               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+       }
+
+       public function testMultiDisk0() {
+               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+                       'Split zip error' );
+       }
+
+       public function testNoSignature() {
+               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+                       'No signature should give "wrong format" error' );
+       }
+
+       public function testSimple() {
+               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+               $this->assertEquals( $this->entries, [ [
+                       'name' => 'Class.class',
+                       'mtime' => '20010115000000',
+                       'size' => 1,
+               ] ] );
+       }
+
+       public function testBadCentralEntrySignature() {
+               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+                       'Bad central entry error' );
+       }
+
+       public function testTrailingBytes() {
+               // Due to T40432 this is now zip-wrong-format instead of zip-bad
+               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
+                       'Trailing bytes error' );
+       }
+
+       public function testWrongCDStart() {
+               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+                       'Wrong CD start disk error' );
+       }
+
+       public function testCentralDirectoryGap() {
+               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+                       'CD gap error' );
+       }
+
+       public function testCentralDirectoryTruncated() {
+               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+                       'CD truncated error (should hit unpack() overrun)' );
+       }
+
+       public function testLooksLikeZip64() {
+               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+                       'A file which looks like ZIP64 but isn\'t, should give error' );
+       }
+}
index 6544544..6e236cd 100755 (executable)
@@ -34,6 +34,13 @@ class PHPUnitMaintClass extends Maintenance {
                );
        }
 
+       public function setup() {
+               parent::setup();
+
+               require_once __DIR__ . '/../common/TestSetup.php';
+               TestSetup::snapshotGlobals();
+       }
+
        public function finalSetup() {
                parent::finalSetup();
 
diff --git a/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 9dff2cc..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\MainSlotRoleHandler;
-use MediaWikiUnitTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\MainSlotRoleHandler
- */
-class MainSlotRoleHandlerTest extends MediaWikiUnitTestCase {
-
-       private function makeTitleObject( $ns ) {
-               /** @var Title|MockObject $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $title->method( 'getNamespace' )
-                       ->willReturn( $ns );
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new MainSlotRoleHandler( [] );
-               $this->assertSame( 'main', $handler->getRole() );
-               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
-        */
-       public function testFetDefaultModel() {
-               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
-
-               // For the main handler, the namespace determins the default model
-               $titleMain = $this->makeTitleObject( NS_MAIN );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
-
-               $title100 = $this->makeTitleObject( 100 );
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               // For the main handler, (nearly) all models are allowed
-               $title = $this->makeTitleObject( NS_MAIN );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               $this->assertTrue( $handler->supportsArticleCount() );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRecordTest.php b/tests/phpunit/unit/includes/Revision/SlotRecordTest.php
deleted file mode 100644 (file)
index aab430a..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Revision\IncompleteRevisionException;
-use MediaWiki\Revision\SlotRecord;
-use MediaWiki\Revision\SuppressedDataException;
-use MediaWikiUnitTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Revision\SlotRecord
- */
-class SlotRecordTest extends MediaWikiUnitTestCase {
-
-       private function makeRow( $data = [] ) {
-               $data = $data + [
-                       'slot_id' => 1234,
-                       'slot_content_id' => 33,
-                       'content_size' => '5',
-                       'content_sha1' => 'someHash',
-                       'content_address' => 'tt:456',
-                       'model_name' => CONTENT_MODEL_WIKITEXT,
-                       'format_name' => CONTENT_FORMAT_WIKITEXT,
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '1',
-                       'role_name' => 'myRole',
-               ];
-               return (object)$data;
-       }
-
-       public function testCompleteConstruction() {
-               $row = $this->makeRow();
-               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasContentId() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertTrue( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 5, $record->getSize() );
-               $this->assertSame( 'someHash', $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 1, $record->getOrigin() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testConstructionDeferred() {
-               $row = $this->makeRow( [
-                       'content_size' => null, // to be computed
-                       'content_sha1' => null, // to be computed
-                       'format_name' => function () {
-                               return CONTENT_FORMAT_WIKITEXT;
-                       },
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '2',
-                       'slot_content_id' => function () {
-                               return null;
-                       },
-               ] );
-
-               $content = function () {
-                       return new WikitextContent( 'A' );
-               };
-
-               $record = new SlotRecord( $row, $content );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testNewUnsaved() {
-               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
-               $this->assertFalse( $record->hasAddress() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->hasRevision() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertFalse( $record->hasOrigin() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotEmpty( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function provideInvalidConstruction() {
-               yield 'both null' => [ null, null ];
-               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
-               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
-               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
-               yield 'null content' => [ (object)[], null ];
-       }
-
-       /**
-        * @dataProvider provideInvalidConstruction
-        */
-       public function testInvalidConstruction( $row, $content ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new SlotRecord( $row, $content );
-       }
-
-       public function testGetContentId_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getContentId();
-       }
-
-       public function testGetAddress_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getAddress();
-       }
-
-       public function provideIncomplete() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               yield 'unsaved' => [ $unsaved ];
-
-               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $inherited = SlotRecord::newInherited( $parent );
-               yield 'inherited' => [ $inherited ];
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetRevision_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getRevision();
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetOrigin_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getOrigin();
-       }
-
-       public function provideHashStability() {
-               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
-               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
-       }
-
-       /**
-        * @dataProvider provideHashStability
-        */
-       public function testHashStability( $text, $hash ) {
-               // Changing the output of the hash function will break things horribly!
-
-               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
-               $this->assertSame( $hash, $record->getSha1() );
-       }
-
-       public function testHashComputed() {
-               $row = $this->makeRow();
-               $row->content_sha1 = '';
-
-               $rec = new SlotRecord( $row, new WikitextContent( 'A' ) );
-               $this->assertNotEmpty( $rec->getSha1() );
-       }
-
-       public function testNewWithSuppressedContent() {
-               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $output = SlotRecord::newWithSuppressedContent( $input );
-
-               $this->setExpectedException( SuppressedDataException::class );
-               $output->getContent();
-       }
-
-       public function testNewInherited() {
-               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
-               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, before saving revision meta-data.
-               $inherited = SlotRecord::newInherited( $parent );
-
-               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
-               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
-               $this->assertSame( $parent->getContent(), $inherited->getContent() );
-               $this->assertTrue( $inherited->isInherited() );
-               $this->assertTrue( $inherited->hasOrigin() );
-               $this->assertFalse( $inherited->hasRevision() );
-
-               // make sure we didn't mess with the internal state of $parent
-               $this->assertFalse( $parent->isInherited() );
-               $this->assertSame( 7, $parent->getRevision() );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved(
-                       10,
-                       $inherited->getContentId(),
-                       $inherited->getAddress(),
-                       $inherited
-               );
-               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
-               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
-               $this->assertSame( $parent->getContent(), $saved->getContent() );
-               $this->assertTrue( $saved->isInherited() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertSame( 10, $saved->getRevision() );
-
-               // make sure we didn't mess with the internal state of $parent or $inherited
-               $this->assertSame( 7, $parent->getRevision() );
-               $this->assertFalse( $inherited->hasRevision() );
-       }
-
-       public function testNewSaved() {
-               // This would happen while doing an edit, before saving revision meta-data.
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
-               $this->assertFalse( $saved->isInherited() );
-               $this->assertTrue( $saved->hasOrigin() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertTrue( $saved->hasAddress() );
-               $this->assertTrue( $saved->hasContentId() );
-               $this->assertSame( 'theNewAddress', $saved->getAddress() );
-               $this->assertSame( 20, $saved->getContentId() );
-               $this->assertSame( 'A', $saved->getContent()->getText() );
-               $this->assertSame( 10, $saved->getRevision() );
-               $this->assertSame( 10, $saved->getOrigin() );
-
-               // make sure we didn't mess with the internal state of $unsaved
-               $this->assertFalse( $unsaved->hasAddress() );
-               $this->assertFalse( $unsaved->hasContentId() );
-               $this->assertFalse( $unsaved->hasRevision() );
-       }
-
-       public function provideNewSaved_LogicException() {
-               $freshRow = $this->makeRow( [
-                       'content_id' => 10,
-                       'content_address' => 'address:1',
-                       'slot_origin' => 1,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
-               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
-               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
-               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
-               $inheritedRow = $this->makeRow( [
-                       'content_id' => null,
-                       'content_address' => null,
-                       'slot_origin' => 0,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
-               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_LogicException
-        */
-       public function testNewSaved_LogicException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( LogicException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideNewSaved_InvalidArgumentException() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
-               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
-               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_InvalidArgumentException
-        */
-       public function testNewSaved_InvalidArgumentException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideHasSameContent() {
-               $fail = function () {
-                       self::fail( 'There should be no need to actually load the content.' );
-               };
-
-               $a100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a1b = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100null = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => null,
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a2 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $b100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'B',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a200a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 200,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100x1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-x',
-                                       'content_address' => 'xxx:x1',
-                               ]
-                       ),
-                       $fail
-               );
-
-               yield 'same instance' => [ $a100a1, $a100a1, true ];
-               yield 'no address' => [ $a100a1, $a100null, true ];
-               yield 'same address' => [ $a100a1, $a100a1b, true ];
-               yield 'different address' => [ $a100a1, $a100a2, true ];
-               yield 'different model' => [ $a100a1, $b100a1, false ];
-               yield 'different size' => [ $a100a1, $a200a1, false ];
-               yield 'different hash' => [ $a100a1, $a100x1, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        */
-       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
-               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
-               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/WikiReferenceTest.php b/tests/phpunit/unit/includes/WikiReferenceTest.php
deleted file mode 100644 (file)
index a4aae86..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-/**
- * @covers WikiReference
- */
-class WikiReferenceTest extends MediaWikiUnitTestCase {
-
-       public function provideGetDisplayName() {
-               return [
-                       'http' => [ 'foo.bar', 'http://foo.bar' ],
-                       'https' => [ 'foo.bar', 'http://foo.bar' ],
-
-                       // apparently, this is the expected behavior
-                       'invalid' => [ 'purple kittens', 'purple kittens' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetDisplayName
-        */
-       public function testGetDisplayName( $expected, $canonicalServer ) {
-               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
-               $this->assertEquals( $expected, $reference->getDisplayName() );
-       }
-
-       public function testGetCanonicalServer() {
-               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
-               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
-       }
-
-       public function provideGetCanonicalUrl() {
-               return [
-                       'no fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               'https://acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               'https://acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               'https://acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               'https://acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        */
-       public function testGetCanonicalUrl(
-               $expected, $canonicalServer, $server, $path, $page, $fragmentId
-       ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        * @note getUrl is an alias for getCanonicalUrl
-        */
-       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
-       }
-
-       public function provideGetFullUrl() {
-               return [
-                       'no fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               '//acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               '//acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               '//acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               '//acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFullUrl
-        */
-       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
deleted file mode 100644 (file)
index 1a8b585..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @covers DifferenceEngineSlotDiffRenderer
- */
-class DifferenceEngineSlotDiffRendererTest extends \MediaWikiUnitTestCase {
-
-       public function testGetDiff() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
-               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
-               $this->assertEquals( 'xxx|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( null, $newContent );
-               $this->assertEquals( '|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
-               $this->assertEquals( 'xxx|', $diff );
-       }
-
-       public function testAddModules() {
-               $output = $this->getMockBuilder( OutputPage::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'addModules' ] )
-                       ->getMock();
-               $output->expects( $this->once() )
-                       ->method( 'addModules' )
-                       ->with( 'foo' );
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $slotDiffRenderer->addModules( $output );
-       }
-
-       public function testGetExtraCacheKeys() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
-               $this->assertSame( [ 'foo' ], $extraCacheKeys );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php
deleted file mode 100644 (file)
index f778115..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-use Wikimedia\Assert\ParameterTypeException;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers SlotDiffRenderer
- */
-class SlotDiffRendererTest extends \MediaWikiUnitTestCase {
-
-       /**
-        * @dataProvider provideNormalizeContents
-        */
-       public function testNormalizeContents(
-               $oldContent, $newContent, $allowedClasses,
-               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
-       ) {
-               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
-                       ->getMock();
-               try {
-                       // __call needs help deciding which parameter to take by reference
-                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
-                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
-                       $this->assertEquals( $expectedOldContent, $oldContent );
-                       $this->assertEquals( $expectedNewContent, $newContent );
-               } catch ( Exception $e ) {
-                       if ( !$expectedExceptionClass ) {
-                               throw $e;
-                       }
-                       $this->assertInstanceOf( $expectedExceptionClass, $e );
-               }
-       }
-
-       public function provideNormalizeContents() {
-               return [
-                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
-                       'left null' => [
-                               null, new WikitextContent( 'abc' ), null,
-                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
-                       ],
-                       'right null' => [
-                               new WikitextContent( 'def' ), null, null,
-                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (subclass)' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (null)' => [
-                               new WikitextContent( 'abc' ), null, TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter failure (left)' => [
-                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter failure (right)' => [
-                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter (array syntax)' => [
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
-                       ],
-                       'type filter failure (array syntax)' => [
-                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               null, null, ParameterTypeException::class,
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php
deleted file mode 100644 (file)
index 6084601..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-class FileBackendDBRepoWrapperTest extends MediaWikiUnitTestCase {
-       protected $backendName = 'foo-backend';
-       protected $repoName = 'pureTestRepo';
-
-       /**
-        * @dataProvider getBackendPathsProvider
-        * @covers FileBackendDBRepoWrapper::getBackendPaths
-        */
-       public function testGetBackendPaths(
-               $mocks,
-               $latest,
-               $dbReadsExpected,
-               $dbReturnValue,
-               $originalPath,
-               $expectedBackendPath,
-               $message ) {
-               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
-
-               $dbMock->expects( $dbReadsExpected )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( $dbReturnValue ) );
-
-               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
-
-               $this->assertEquals(
-                       $expectedBackendPath,
-                       $newPaths[0],
-                       $message );
-       }
-
-       public function getBackendPathsProvider() {
-               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
-               $mocksForCaching = $this->getMocks();
-
-               return [
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Public path translated correctly',
-                       ],
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'LRU cache leveraged',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Latest obtained',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-deleted/f/o/foobar.jpg',
-                               $prefix . '-original/f/o/o/foobar',
-                               'Deleted path translated correctly',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               null,
-                               $prefix . '-public/b/a/baz.jpg',
-                               $prefix . '-public/b/a/baz.jpg',
-                               'Path left untouched if no sha1 can be found',
-                       ],
-               ];
-       }
-
-       /**
-        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
-        */
-       public function testGetFileContentsMulti() {
-               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
-
-               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
-               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-public/f/o/foobar.jpg';
-
-               $dbMock->expects( $this->once() )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
-
-               $backendMock->expects( $this->once() )
-                       ->method( 'getFileContentsMulti' )
-                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
-
-               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
-
-               $this->assertEquals(
-                       [ $filenamePath => 'foo' ],
-                       $result,
-                       'File contents paths translated properly'
-               );
-       }
-
-       protected function getMocks() {
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
-                       ->disableOriginalClone()
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $backendMock = $this->getMockBuilder( FSFileBackend::class )
-                       ->setConstructorArgs( [ [
-                                       'name' => $this->backendName,
-                                       'wikiId' => wfWikiID()
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
-                       ->setMethods( [ 'getDB' ] )
-                       ->setConstructorArgs( [ [
-                                       'backend' => $backendMock,
-                                       'repoName' => $this->repoName,
-                                       'dbHandleFactory' => null
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
-
-               return [ $dbMock, $backendMock, $wrapperMock ];
-       }
-}
diff --git a/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php
deleted file mode 100644 (file)
index 365c140..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-/**
- * @todo Could use a test of extended XMP segments. Hard to find programs that
- * create example files, and creating my own in vim propbably wouldn't
- * serve as a very good "test". (Adobe photoshop probably creates such files
- * but it costs money). The implementation of it currently in MediaWiki is based
- * solely on reading the standard, without any real world test files.
- *
- * @group Media
- * @covers JpegMetadataExtractor
- */
-class JpegMetadataExtractorTest extends MediaWikiUnitTestCase {
-
-       protected $filePath;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->filePath = __DIR__ . '/../../../data/media/';
-       }
-
-       /**
-        * We also use this test to test padding bytes don't
-        * screw stuff up
-        *
-        * @param string $file Filename
-        *
-        * @dataProvider provideUtf8Comment
-        */
-       public function testUtf8Comment( $file ) {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
-               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
-       }
-
-       public static function provideUtf8Comment() {
-               return [
-                       [ 'jpeg-comment-utf.jpg' ],
-                       [ 'jpeg-padding-even.jpg' ],
-                       [ 'jpeg-padding-odd.jpg' ],
-               ];
-       }
-
-       /** The file is iso-8859-1, but it should get auto converted */
-       public function testIso88591Comment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
-               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
-       }
-
-       /** Comment values that are non-textual (random binary junk) should not be shown.
-        * The example test file has a comment with a 0x5 byte in it which is a control character
-        * and considered binary junk for our purposes.
-        */
-       public function testBinaryCommentStripped() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
-               $this->assertEmpty( $res['COM'] );
-       }
-
-       /* Very rarely a file can have multiple comments.
-        *   Order of comments is based on order inside the file.
-        */
-       public function testMultipleComment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
-               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
-       }
-
-       public function testXMPExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testPSIRExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = '50686f746f73686f7020332e30003842494d04040000000'
-                       . '000181c02190004746573741c02190003666f6f1c020000020004';
-               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
-       }
-
-       public function testXMPExtractionAltAppId() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testIPTCHashComparisionNoHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-no-hash', $res );
-       }
-
-       public function testIPTCHashComparisionBadHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-bad-hash', $res );
-       }
-
-       public function testIPTCHashComparisionGoodHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-good-hash', $res );
-       }
-
-       public function testExifByteOrder() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
-               $expected = 'BE';
-               $this->assertEquals( $expected, $res['byteOrder'] );
-       }
-
-       public function testInfiniteRead() {
-               // test file truncated right after a segment, which previously
-               // caused an infinite loop looking for the next segment byte.
-               // Should get past infinite loop and throw in wfUnpack()
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
-       }
-
-       public function testInfiniteRead2() {
-               // test file truncated after a segment's marker and size, which
-               // would cause a seek past end of file. Seek past end of file
-               // doesn't actually fail, but prevents further reading and was
-               // devolving into the previous case (testInfiniteRead).
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
-       }
-}
diff --git a/tests/phpunit/unit/includes/parser/ParserFactoryTest.php b/tests/phpunit/unit/includes/parser/ParserFactoryTest.php
deleted file mode 100644 (file)
index f1e48c7..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @covers ParserFactory
- */
-class ParserFactoryTest extends MediaWikiUnitTestCase {
-       use FactoryArgTestTrait;
-
-       protected static function getFactoryClass() {
-               return ParserFactory::class;
-       }
-
-       protected static function getInstanceClass() {
-               return Parser::class;
-       }
-
-       protected static function getFactoryMethodName() {
-               return 'create';
-       }
-
-       protected static function getExtraClassArgCount() {
-               // The parser factory itself is passed to the parser
-               return 1;
-       }
-
-       protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
-               if ( $param->getPosition() === 0 ) {
-                       return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ];
-               }
-               return [];
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php
deleted file mode 100644 (file)
index d426306..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-use MediaWiki\Site\MediaWikiPageNameNormalizer;
-
-/**
- * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @since 1.27
- *
- * @group Site
- * @group medium
- *
- * @author Marius Hoch
- */
-class MediaWikiPageNameNormalizerTest extends MediaWikiUnitTestCase {
-
-       /**
-        * @dataProvider normalizePageTitleProvider
-        */
-       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
-               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
-
-               $normalizer = new MediaWikiPageNameNormalizer(
-                       new MediaWikiPageNameNormalizerTestMockHttp()
-               );
-
-               $this->assertSame(
-                       $expected,
-                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
-               );
-       }
-
-       public function normalizePageTitleProvider() {
-               // Response are taken from wikidata and kkwiki using the following API request
-               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
-               return [
-                       'universe (Q1)' => [
-                               'Q1',
-                               'Q1',
-                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
-                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
-                       ],
-                       'Q404 redirects to Q395' => [
-                               'Q395',
-                               'Q404',
-                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
-                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
-                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
-                       ],
-                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
-                               'Д',
-                               'D',
-                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
-                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
-                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
-                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
-                               . '"lastrevid":2373618,"length":3501}}}}'
-                       ],
-                       'there is no Q0' => [
-                               false,
-                               'Q0',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
-                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
-                       ],
-                       'invalid title' => [
-                               false,
-                               '{{',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
-                               . '"invalidreason":"The requested page title contains invalid '
-                               . 'characters: \"{\".","invalid":""}}}}'
-                       ],
-                       'error on get' => [ false, 'ABC', false ]
-               ];
-       }
-
-}
-
-/**
- * @private
- * @see Http
- */
-class MediaWikiPageNameNormalizerTestMockHttp extends Http {
-
-       /**
-        * @var mixed
-        */
-       public static $response;
-
-       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
-
-               return self::$response;
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/SiteExporterTest.php b/tests/phpunit/unit/includes/site/SiteExporterTest.php
deleted file mode 100644 (file)
index dcf51ac..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteExporter
- *
- * @author Daniel Kinzler
- */
-class SiteExporterTest extends MediaWikiUnitTestCase {
-
-       public function testConstructor_InvalidArgument() {
-               $this->setExpectedException( InvalidArgumentException::class );
-
-               new SiteExporter( 'Foo' );
-       }
-
-       public function testExportSites() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( [ $foo, $acme ] );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $this->assertContains( '<sites ', $xml );
-               $this->assertContains( '<site>', $xml );
-               $this->assertContains( '<globalid>Foo</globalid>', $xml );
-               $this->assertContains( '</site>', $xml );
-               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
-               $this->assertContains( '<group>Test</group>', $xml );
-               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
-               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
-               $this->assertContains( '</sites>', $xml );
-
-               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
-               $xsdFile = __DIR__ . '/../../../../../docs/sitelist-1.0.xsd';
-               $xsdData = file_get_contents( $xsdFile );
-
-               $document = new DOMDocument();
-               $document->loadXML( $xml, LIBXML_NONET );
-               $document->schemaValidateSource( $xsdData );
-       }
-
-       private function newSiteStore( SiteList $sites ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
-                               foreach ( $moreSites as $site ) {
-                                       $sites->setSite( $site );
-                               }
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               return $store;
-       }
-
-       public function provideRoundTrip() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               new SiteList()
-                       ],
-
-                       'some' => [
-                               new SiteList( [ $foo, $acme, $dewiki ] ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideRoundTrip()
-        */
-       public function testRoundTrip( SiteList $sites ) {
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( $sites );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $actualSites = new SiteList();
-               $store = $this->newSiteStore( $actualSites );
-
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( $xml );
-
-               $this->assertEquals( $sites, $actualSites );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.php b/tests/phpunit/unit/includes/site/SiteImporterTest.php
deleted file mode 100644 (file)
index d4e4103..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteImporter
- *
- * @author Daniel Kinzler
- */
-class SiteImporterTest extends MediaWikiUnitTestCase {
-
-       private function newSiteImporter( array $expectedSites, $errorCount ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
-                               $this->assertSitesEqual( $expectedSites, $sites );
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
-               $errorHandler->expects( $this->exactly( $errorCount ) )
-                       ->method( 'error' );
-
-               $importer = new SiteImporter( $store );
-               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
-
-               return $importer;
-       }
-
-       public function assertSitesEqual( $expected, $actual, $message = '' ) {
-               $this->assertEquals(
-                       $this->getSerializedSiteList( $expected ),
-                       $this->getSerializedSiteList( $actual ),
-                       $message
-               );
-       }
-
-       public function provideImportFromXML() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               '<sites></sites>',
-                               [],
-                       ],
-                       'no sites' => [
-                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
-                               [],
-                       ],
-                       'minimal' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                               '</sites>',
-                               [ $foo ],
-                       ],
-                       'full' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                                       '<site type="mediawiki">' .
-                                               '<source>meta.wikimedia.org</source>' .
-                                               '<globalid>dewiki</globalid>' .
-                                               '<localid type="interwiki">wikipedia</localid>' .
-                                               '<localid type="equivalent">de</localid>' .
-                                               '<group>wikipedia</group>' .
-                                               '<forward/>' .
-                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
-                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme, $dewiki ],
-                       ],
-                       'skip' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site><barf>Foo</barf></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<silly>boop!</silly>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme ],
-                               1
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideImportFromXML
-        */
-       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
-               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
-               $importer->importFromXML( $xml );
-       }
-
-       public function testImportFromXML_malformed() {
-               $this->setExpectedException( Exception::class );
-
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( 'THIS IS NOT XML' );
-       }
-
-       public function testImportFromFile() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
-
-               $file = __DIR__ . '/SiteImporterTest.xml';
-               $importer->importFromFile( $file );
-       }
-
-       /**
-        * @param Site[] $sites
-        *
-        * @return array[]
-        */
-       private function getSerializedSiteList( $sites ) {
-               $serialized = [];
-
-               foreach ( $sites as $site ) {
-                       $key = $site->getGlobalId();
-                       $data = unserialize( $site->serialize() );
-
-                       $serialized[$key] = $data;
-               }
-
-               return $serialized;
-       }
-}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.xml b/tests/phpunit/unit/includes/site/SiteImporterTest.xml
deleted file mode 100644 (file)
index 720b1fa..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
-       <site><globalid>Foo</globalid></site>
-       <site>
-               <globalid>acme.com</globalid>
-               <localid type="interwiki">acme</localid>
-               <group>Test</group>
-               <path type="link">http://acme.com/</path>
-       </site>
-       <site type="mediawiki">
-               <source>meta.wikimedia.org</source>
-               <globalid>dewiki</globalid>
-               <localid type="interwiki">wikipedia</localid>
-               <localid type="equivalent">de</localid>
-               <group>wikipedia</group>
-               <forward/>
-               <path type="link">http://de.wikipedia.org/w/</path>
-               <path type="page_path">http://de.wikipedia.org/wiki/</path>
-       </site>
-</sites>
diff --git a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php
deleted file mode 100644 (file)
index be7e224..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * @covers ZipDirectoryReader
- * NOTE: this test is more like an integration test than a unit test
- */
-class ZipDirectoryReaderTest extends MediaWikiUnitTestCase {
-
-       protected $zipDir;
-       protected $entries;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->zipDir = __DIR__ . '/../../../data/zip';
-       }
-
-       function zipCallback( $entry ) {
-               $this->entries[] = $entry;
-       }
-
-       function readZipAssertError( $file, $error, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
-       }
-
-       function readZipAssertSuccess( $file, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->isOK(), $assertMessage );
-       }
-
-       public function testEmpty() {
-               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
-       }
-
-       public function testMultiDisk0() {
-               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
-                       'Split zip error' );
-       }
-
-       public function testNoSignature() {
-               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
-                       'No signature should give "wrong format" error' );
-       }
-
-       public function testSimple() {
-               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
-               $this->assertEquals( $this->entries, [ [
-                       'name' => 'Class.class',
-                       'mtime' => '20010115000000',
-                       'size' => 1,
-               ] ] );
-       }
-
-       public function testBadCentralEntrySignature() {
-               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
-                       'Bad central entry error' );
-       }
-
-       public function testTrailingBytes() {
-               // Due to T40432 this is now zip-wrong-format instead of zip-bad
-               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
-                       'Trailing bytes error' );
-       }
-
-       public function testWrongCDStart() {
-               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
-                       'Wrong CD start disk error' );
-       }
-
-       public function testCentralDirectoryGap() {
-               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
-                       'CD gap error' );
-       }
-
-       public function testCentralDirectoryTruncated() {
-               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
-                       'CD truncated error (should hit unpack() overrun)' );
-       }
-
-       public function testLooksLikeZip64() {
-               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
-                       'A file which looks like ZIP64 but isn\'t, should give error' );
-       }
-}