From: Timo Tijhof Date: Wed, 7 Aug 2019 13:40:55 +0000 (+0100) Subject: phpunit: Repair GLOBALS reset in MediaWikiUnitTestCase X-Git-Tag: 1.34.0-rc.0~428^2 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=29e0183a56119956ac04fe6ffe2436265fa940d2;p=lhc%2Fweb%2Fwiklou.git phpunit: Repair GLOBALS reset in MediaWikiUnitTestCase 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 --- diff --git a/tests/common/TestSetup.php b/tests/common/TestSetup.php index d4df8ae5ee..141e307fbe 100644 --- a/tests/common/TestSetup.php +++ b/tests/common/TestSetup.php @@ -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 diff --git a/tests/phpunit/MediaWikiUnitTestCase.php b/tests/phpunit/MediaWikiUnitTestCase.php index bb018aaf14..3f876ae4aa 100644 --- a/tests/phpunit/MediaWikiUnitTestCase.php +++ b/tests/phpunit/MediaWikiUnitTestCase.php @@ -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. diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 9e79496ac5..477dbd298e 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -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 index 0000000000..60d456d1a3 --- /dev/null +++ b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php @@ -0,0 +1,78 @@ +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 index 0000000000..7ffe0048e3 --- /dev/null +++ b/tests/phpunit/includes/Revision/SlotRecordTest.php @@ -0,0 +1,415 @@ + 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 index 0000000000..702d3d7d89 --- /dev/null +++ b/tests/phpunit/includes/WikiReferenceTest.php @@ -0,0 +1,164 @@ + [ '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 index 0000000000..e5c23d172b --- /dev/null +++ b/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php @@ -0,0 +1,44 @@ +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 index 0000000000..9f15517c99 --- /dev/null +++ b/tests/phpunit/includes/diff/SlotDiffRendererTest.php @@ -0,0 +1,78 @@ +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 index 0000000000..69fc3679b3 --- /dev/null +++ b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php @@ -0,0 +1,140 @@ +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 index 0000000000..6c56510170 --- /dev/null +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -0,0 +1,128 @@ +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 index 0000000000..e6e9db41eb --- /dev/null +++ b/tests/phpunit/includes/parser/ParserFactoryTest.php @@ -0,0 +1,32 @@ +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 index 0000000000..7a6647b48b --- /dev/null +++ b/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php @@ -0,0 +1,114 @@ +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 index 0000000000..158be69cd6 --- /dev/null +++ b/tests/phpunit/includes/site/SiteExporterTest.php @@ -0,0 +1,145 @@ +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( 'assertContains( '', $xml ); + $this->assertContains( 'Foo', $xml ); + $this->assertContains( '', $xml ); + $this->assertContains( 'acme.com', $xml ); + $this->assertContains( 'Test', $xml ); + $this->assertContains( 'acme', $xml ); + $this->assertContains( 'http://acme.com/', $xml ); + $this->assertContains( '', $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 index 0000000000..c614dd43a0 --- /dev/null +++ b/tests/phpunit/includes/site/SiteImporterTest.php @@ -0,0 +1,197 @@ +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' => [ + '', + [], + ], + 'no sites' => [ + 'FooBla', + [], + ], + 'minimal' => [ + '' . + 'Foo' . + '', + [ $foo ], + ], + 'full' => [ + '' . + 'Foo' . + '' . + 'acme.com' . + 'acme' . + 'Test' . + 'http://acme.com/' . + '' . + '' . + 'meta.wikimedia.org' . + 'dewiki' . + 'wikipedia' . + 'de' . + 'wikipedia' . + '' . + 'http://de.wikipedia.org/w/' . + 'http://de.wikipedia.org/wiki/' . + '' . + '', + [ $foo, $acme, $dewiki ], + ], + 'skip' => [ + '' . + 'Foo' . + 'Foo' . + '' . + 'acme.com' . + 'acme' . + 'boop!' . + 'Test' . + 'http://acme.com/' . + '' . + '', + [ $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 index 0000000000..720b1faf1a --- /dev/null +++ b/tests/phpunit/includes/site/SiteImporterTest.xml @@ -0,0 +1,19 @@ + + Foo + + acme.com + acme + Test + http://acme.com/ + + + meta.wikimedia.org + dewiki + wikipedia + de + wikipedia + + http://de.wikipedia.org/w/ + http://de.wikipedia.org/wiki/ + + diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php new file mode 100644 index 0000000000..492b2509ce --- /dev/null +++ b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php @@ -0,0 +1,85 @@ +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' ); + } +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php index 65445447f5..6e236cdbb6 100755 --- a/tests/phpunit/phpunit.php +++ b/tests/phpunit/phpunit.php @@ -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 index 9dff2cc500..0000000000 --- a/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php +++ /dev/null @@ -1,79 +0,0 @@ -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 index aab430aaa6..0000000000 --- a/tests/phpunit/unit/includes/Revision/SlotRecordTest.php +++ /dev/null @@ -1,416 +0,0 @@ - 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 index a4aae86c0f..0000000000 --- a/tests/phpunit/unit/includes/WikiReferenceTest.php +++ /dev/null @@ -1,164 +0,0 @@ - [ '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 index 1a8b58590a..0000000000 --- a/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php +++ /dev/null @@ -1,44 +0,0 @@ -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 index f778115780..0000000000 --- a/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php +++ /dev/null @@ -1,78 +0,0 @@ -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 index 6084601b05..0000000000 --- a/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php +++ /dev/null @@ -1,140 +0,0 @@ -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 index 365c140405..0000000000 --- a/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php +++ /dev/null @@ -1,128 +0,0 @@ -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 index f1e48c7388..0000000000 --- a/tests/phpunit/unit/includes/parser/ParserFactoryTest.php +++ /dev/null @@ -1,32 +0,0 @@ -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 index d426306e3f..0000000000 --- a/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php +++ /dev/null @@ -1,114 +0,0 @@ -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 index dcf51ac541..0000000000 --- a/tests/phpunit/unit/includes/site/SiteExporterTest.php +++ /dev/null @@ -1,145 +0,0 @@ -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( 'assertContains( '', $xml ); - $this->assertContains( 'Foo', $xml ); - $this->assertContains( '', $xml ); - $this->assertContains( 'acme.com', $xml ); - $this->assertContains( 'Test', $xml ); - $this->assertContains( 'acme', $xml ); - $this->assertContains( 'http://acme.com/', $xml ); - $this->assertContains( '', $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 index d4e4103c39..0000000000 --- a/tests/phpunit/unit/includes/site/SiteImporterTest.php +++ /dev/null @@ -1,197 +0,0 @@ -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' => [ - '', - [], - ], - 'no sites' => [ - 'FooBla', - [], - ], - 'minimal' => [ - '' . - 'Foo' . - '', - [ $foo ], - ], - 'full' => [ - '' . - 'Foo' . - '' . - 'acme.com' . - 'acme' . - 'Test' . - 'http://acme.com/' . - '' . - '' . - 'meta.wikimedia.org' . - 'dewiki' . - 'wikipedia' . - 'de' . - 'wikipedia' . - '' . - 'http://de.wikipedia.org/w/' . - 'http://de.wikipedia.org/wiki/' . - '' . - '', - [ $foo, $acme, $dewiki ], - ], - 'skip' => [ - '' . - 'Foo' . - 'Foo' . - '' . - 'acme.com' . - 'acme' . - 'boop!' . - 'Test' . - 'http://acme.com/' . - '' . - '', - [ $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 index 720b1faf1a..0000000000 --- a/tests/phpunit/unit/includes/site/SiteImporterTest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - Foo - - acme.com - acme - Test - http://acme.com/ - - - meta.wikimedia.org - dewiki - wikipedia - de - wikipedia - - http://de.wikipedia.org/w/ - http://de.wikipedia.org/wiki/ - - diff --git a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php deleted file mode 100644 index be7e224e03..0000000000 --- a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php +++ /dev/null @@ -1,86 +0,0 @@ -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' ); - } -}