getServiceContainer(); $store = new SqlBlobStore( $services->getDBLoadBalancer(), $extStore ?: $services->getExternalStoreAccess(), $cache ?: $services->getMainWANObjectCache() ); return $store; } public function testGetSetCompressRevisions() { $store = $this->getBlobStore(); $this->assertFalse( $store->getCompressBlobs() ); $store->setCompressBlobs( true ); $this->assertTrue( $store->getCompressBlobs() ); } public function testGetSetLegacyEncoding() { $store = $this->getBlobStore(); $this->assertFalse( $store->getLegacyEncoding() ); $store->setLegacyEncoding( 'foo' ); $this->assertSame( 'foo', $store->getLegacyEncoding() ); } public function testGetSetCacheExpiry() { $store = $this->getBlobStore(); $this->assertSame( 604800, $store->getCacheExpiry() ); $store->setCacheExpiry( 12 ); $this->assertSame( 12, $store->getCacheExpiry() ); } public function testGetSetUseExternalStore() { $store = $this->getBlobStore(); $this->assertFalse( $store->getUseExternalStore() ); $store->setUseExternalStore( true ); $this->assertTrue( $store->getUseExternalStore() ); } private function makeObjectBlob( $text ) { $obj = new ConcatenatedGzipHistoryBlob(); $obj->setText( $text ); return serialize( $obj ); } public function provideDecompress() { yield '(no legacy encoding), empty in empty out' => [ false, '', [], '' ]; yield '(no legacy encoding), string in string out' => [ false, 'A', [], 'A' ]; yield '(no legacy encoding), error flag -> false' => [ false, 'X', [ 'error' ], false ]; yield '(no legacy encoding), string in with gzip flag returns string' => [ // gzip string below generated with gzdeflate( 'AAAABBAAA' ) false, "sttttr\002\022\000", [ 'gzip' ], 'AAAABBAAA', ]; yield '(no legacy encoding), string in with object flag returns false' => [ // gzip string below generated with serialize( 'JOJO' ) false, "s:4:\"JOJO\";", [ 'object' ], false, ]; yield '(no legacy encoding), serialized object in with object flag returns string' => [ false, $this->makeObjectBlob( 'HHJJDDFF' ), [ 'object' ], 'HHJJDDFF', ]; yield '(no legacy encoding), serialized object in with object & gzip flag returns string' => [ false, gzdeflate( $this->makeObjectBlob( '8219JJJ840' ) ), [ 'object', 'gzip' ], '8219JJJ840', ]; yield '(ISO-8859-1 encoding), string in string out' => [ 'ISO-8859-1', iconv( 'utf-8', 'ISO-8859-1', "1®Àþ1" ), [], '1®Àþ1', ]; yield '(ISO-8859-1 encoding), serialized object in with gzip flags returns string' => [ 'ISO-8859-1', gzdeflate( iconv( 'utf-8', 'ISO-8859-1', "4®Àþ4" ) ), [ 'gzip' ], '4®Àþ4', ]; yield '(ISO-8859-1 encoding), serialized object in with object flags returns string' => [ 'ISO-8859-1', $this->makeObjectBlob( iconv( 'utf-8', 'ISO-8859-1', "3®Àþ3" ) ), [ 'object' ], '3®Àþ3', ]; yield '(ISO-8859-1 encoding), serialized object in with object & gzip flags returns string' => [ 'ISO-8859-1', gzdeflate( $this->makeObjectBlob( iconv( 'utf-8', 'ISO-8859-1', "2®Àþ2" ) ) ), [ 'gzip', 'object' ], '2®Àþ2', ]; yield 'T184749 (windows-1252 encoding), string in string out' => [ 'windows-1252', iconv( 'utf-8', 'windows-1252', "sammansättningar" ), [], 'sammansättningar', ]; yield 'T184749 (windows-1252 encoding), string in string out with gzip' => [ 'windows-1252', gzdeflate( iconv( 'utf-8', 'windows-1252', "sammansättningar" ) ), [ 'gzip' ], 'sammansättningar', ]; } /** * @dataProvider provideDecompress * @param string|bool $legacyEncoding * @param mixed $data * @param array $flags * @param mixed $expected */ public function testDecompressData( $legacyEncoding, $data, $flags, $expected ) { $store = $this->getBlobStore(); if ( $legacyEncoding ) { $store->setLegacyEncoding( $legacyEncoding ); } $this->assertSame( $expected, $store->decompressData( $data, $flags ) ); } public function testCompressRevisionTextUtf8() { $store = $this->getBlobStore(); $row = (object)[ 'old_text' => "Wiki est l'\xc3\xa9cole superieur !" ]; $row->old_flags = $store->compressData( $row->old_text ); $this->assertStringContainsString( 'utf-8', $row->old_flags, "Flags should contain 'utf-8'" ); $this->assertStringNotContainsString( 'gzip', $row->old_flags, "Flags should not contain 'gzip'" ); $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", $row->old_text, "Direct check" ); } /** * @requires extension zlib */ public function testCompressRevisionTextUtf8Gzip() { $store = $this->getBlobStore(); $store->setCompressBlobs( true ); $row = (object)[ 'old_text' => "Wiki est l'\xc3\xa9cole superieur !" ]; $row->old_flags = $store->compressData( $row->old_text ); $this->assertStringContainsString( 'utf-8', $row->old_flags, "Flags should contain 'utf-8'" ); $this->assertStringContainsString( 'gzip', $row->old_flags, "Flags should contain 'gzip'" ); $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", gzinflate( $row->old_text ), "Direct check" ); } public static function provideBlobs() { yield [ '' ]; yield [ 'someText' ]; yield [ "söme\ntäxt" ]; } public function testSimpleStoreGetBlobKnownBad() { $store = $this->getBlobStore(); $this->expectException( BadBlobException::class ); $store->getBlob( 'bad:lost?bug=T12345' ); } /** * @param string $blob * @dataProvider provideBlobs */ public function testSimpleStoreGetBlobSimpleRoundtrip( $blob ) { $store = $this->getBlobStore(); $address = $store->storeBlob( $blob ); $this->assertSame( $blob, $store->getBlob( $address ) ); } public function testSimpleStorageGetBlobBatchSimpleEmpty() { $store = $this->getBlobStore(); $this->assertArrayEquals( [], $store->getBlobBatch( [] )->getValue() ); } /** * @param string $blob * @dataProvider provideBlobs */ public function testSimpleStorageGetBlobBatchSimpleRoundtrip( $blob ) { $store = $this->getBlobStore(); $addresses = [ $store->storeBlob( $blob ), $store->storeBlob( $blob . '1' ) ]; $this->assertArrayEquals( array_combine( $addresses, [ $blob, $blob . '1' ] ), $store->getBlobBatch( $addresses )->getValue() ); } public function testCachingConsistency() { $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); $store = $this->getBlobStore( $cache ); $addrA = $store->storeBlob( 'A' ); $addrB = $store->storeBlob( 'B' ); $addrC = $store->storeBlob( 'C' ); $addrD = $store->storeBlob( 'D' ); $addrX = 'tt:0'; $dataZ = "söme\ntäxt!"; $addrZ = $store->storeBlob( $dataZ ); $this->assertArrayEquals( [ $addrA => 'A', $addrC => 'C', $addrX => null ], $store->getBlobBatch( [ $addrA, $addrC, $addrX ] )->getValue(), false, true ); $this->assertEquals( 'A', $store->getBlob( $addrA ) ); $this->assertEquals( 'B', $store->getBlob( $addrB ) ); $this->assertEquals( 'C', $store->getBlob( $addrC ) ); $this->assertArrayEquals( [ $addrB => 'B', $addrC => 'C', $addrD => 'D' ], $store->getBlobBatch( [ $addrB, $addrC, $addrD ] )->getValue(), false, true ); $this->assertEquals( $dataZ, $store->getBlob( $addrZ ) ); $this->assertArrayEquals( [ $addrA => 'A', $addrZ => $dataZ ], $store->getBlobBatch( [ $addrA, $addrZ ] )->getValue(), false, true ); } public function testSimpleStorageNonExistentBlob() { $this->expectException( BlobAccessException::class ); $store = $this->getBlobStore(); $store->getBlob( 'tt:this_will_not_exist' ); } public function testSimpleStorageNonExistentBlobBatch() { $store = $this->getBlobStore(); $result = $store->getBlobBatch( [ 'tt:this_will_not_exist', 'tt:0', 'tt:-1', 'tt:10000', 'bla:1001' ] ); $resultBlobs = $result->getValue(); $expected = [ 'tt:this_will_not_exist' => null, 'tt:0' => null, 'tt:-1' => null, 'tt:10000' => null, 'bla:1001' => null ]; ksort( $expected ); ksort( $resultBlobs ); $this->assertSame( $expected, $resultBlobs ); $this->assertStatusMessagesExactly( StatusValue::newGood() ->warning( 'internalerror', 'Bad blob address: tt:this_will_not_exist. Use findBadBlobs.php to remedy.' ) ->warning( 'internalerror', 'Bad blob address: tt:0. Use findBadBlobs.php to remedy.' ) ->warning( 'internalerror', 'Bad blob address: tt:-1. Use findBadBlobs.php to remedy.' ) ->warning( 'internalerror', 'Unknown blob address schema: bla. Use findBadBlobs.php to remedy.' ) ->warning( 'internalerror', 'Unable to fetch blob at tt:10000. Use findBadBlobs.php to remedy.' ), $result ); } public function testSimpleStoragePartialNonExistentBlobBatch() { $store = $this->getBlobStore(); $address = $store->storeBlob( 'test_data' ); $result = $store->getBlobBatch( [ $address, 'tt:this_will_not_exist_too' ] ); $resultBlobs = $result->getValue(); $expected = [ $address => 'test_data', 'tt:this_will_not_exist_too' => null ]; ksort( $expected ); ksort( $resultBlobs ); $this->assertSame( $expected, $resultBlobs ); $this->assertStatusMessagesExactly( StatusValue::newGood() ->warning( 'internalerror', 'Bad blob address: tt:this_will_not_exist_too. Use findBadBlobs.php to remedy.' ), $result ); } /** * @dataProvider provideBlobs */ public function testSimpleStoreGetBlobSimpleRoundtripWindowsLegacyEncoding( $blob ) { $store = $this->getBlobStore(); $store->setLegacyEncoding( 'windows-1252' ); $address = $store->storeBlob( $blob ); $this->assertSame( $blob, $store->getBlob( $address ) ); } /** * @dataProvider provideBlobs */ public function testSimpleStoreGetBlobSimpleRoundtripWindowsLegacyEncodingGzip( $blob ) { // FIXME: fails under postgres - T298692 $this->markTestSkippedIfDbType( 'postgres' ); $store = $this->getBlobStore(); $store->setLegacyEncoding( 'windows-1252' ); $store->setCompressBlobs( true ); $address = $store->storeBlob( $blob ); $this->assertSame( $blob, $store->getBlob( $address ) ); } public static function provideGetTextIdFromAddress() { yield [ 'tt:17', 17 ]; yield [ 'xy:17', null ]; yield [ 'xy:xyzzy', null ]; } /** * @dataProvider provideGetTextIdFromAddress */ public function testGetTextIdFromAddress( $address, $textId ) { $store = $this->getBlobStore(); $this->assertSame( $textId, $store->getTextIdFromAddress( $address ) ); } public static function provideGetTextIdFromAddressInvalidArgumentException() { yield [ 'tt:xy' ]; yield [ 'tt:0' ]; yield [ 'tt:' ]; yield [ 'xy' ]; yield [ '' ]; } /** * @dataProvider provideGetTextIdFromAddressInvalidArgumentException */ public function testGetTextIdFromAddressInvalidArgumentException( $address ) { $this->expectException( InvalidArgumentException::class ); $store = $this->getBlobStore(); $store->getTextIdFromAddress( $address ); } public function testMakeAddressFromTextId() { $this->assertSame( 'tt:17', SqlBlobStore::makeAddressFromTextId( 17 ) ); } public static function providerSplitBlobAddress() { yield [ 'tt:123', 'tt', '123', [] ]; yield [ 'bad:foo?x=y', 'bad', 'foo', [ 'x' => 'y' ] ]; yield [ 'http://test.com/foo/bar?a=b', 'http', 'test.com/foo/bar', [ 'a' => 'b' ] ]; } /** * @dataProvider providerSplitBlobAddress */ public function testSplitBlobAddress( $address, $schema, $id, $parameters ) { $this->assertSame( 'tt:17', SqlBlobStore::makeAddressFromTextId( 17 ) ); } public static function provideExpandBlob() { yield 'Generic test' => [ 'This is a goat of revision text.', 'old_flags' => '', 'old_text' => 'This is a goat of revision text.', ]; } /** * @dataProvider provideExpandBlob */ public function testExpandBlob( $expected, $flags, $raw ) { $blobStore = $this->getBlobStore(); $this->assertEquals( $expected, $blobStore->expandBlob( $raw, explode( ',', $flags ) ) ); } public static function provideExpandBlobWithZlibExtension() { yield 'Generic gzip test' => [ 'This is a small goat of revision text.', 'old_flags' => 'gzip', 'old_text' => gzdeflate( 'This is a small goat of revision text.' ), ]; } /** * @dataProvider provideExpandBlobWithZlibExtension * @requires extension zlib */ public function testGetRevisionWithZlibExtension( $expected, $flags, $raw ) { $blobStore = $this->getBlobStore(); $this->assertEquals( $expected, $blobStore->expandBlob( $raw, explode( ',', $flags ) ) ); } public static function provideExpandBlobWithZlibExtension_badData() { yield 'Generic gzip test' => [ 'old_flags' => 'gzip', 'old_text' => 'DEAD BEEF', ]; } /** * @dataProvider provideExpandBlobWithZlibExtension_badData * @requires extension zlib */ public function testGetRevisionWithZlibExtension_badData( $flags, $raw ) { $blobStore = $this->getBlobStore(); $this->assertFalse( @$blobStore->expandBlob( $raw, explode( ',', $flags ) ) ); } public static function provideExpandBlobWithLegacyEncoding() { yield 'Utf8Native' => [ "Wiki est l'\xc3\xa9cole superieur !", 'iso-8859-1', 'old_flags' => 'utf-8', 'old_text' => "Wiki est l'\xc3\xa9cole superieur !", ]; yield 'Utf8Legacy' => [ "Wiki est l'\xc3\xa9cole superieur !", 'iso-8859-1', 'old_flags' => '', 'old_text' => "Wiki est l'\xe9cole superieur !", ]; } /** * @dataProvider provideExpandBlobWithLegacyEncoding */ public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $flags, $raw ) { $blobStore = $this->getBlobStore(); $blobStore->setLegacyEncoding( $encoding ); $this->assertEquals( $expected, $blobStore->expandBlob( $raw, explode( ',', $flags ) ) ); } public static function provideExpandBlobWithGzipAndLegacyEncoding() { /** * WARNING! * Do not set the external flag! * Otherwise, getRevisionText will hit the live database (if ExternalStore is enabled)! */ yield 'Utf8NativeGzip' => [ "Wiki est l'\xc3\xa9cole superieur !", 'iso-8859-1', 'old_flags' => 'gzip,utf-8', 'old_text' => gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ), ]; yield 'Utf8LegacyGzip' => [ "Wiki est l'\xc3\xa9cole superieur !", 'iso-8859-1', 'old_flags' => 'gzip', 'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ), ]; } /** * @dataProvider provideExpandBlobWithGzipAndLegacyEncoding * @requires extension zlib */ public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $flags, $raw ) { $blobStore = $this->getBlobStore(); $blobStore->setLegacyEncoding( $encoding ); $this->assertEquals( $expected, $blobStore->expandBlob( $raw, explode( ',', $flags ) ) ); } public static function provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal() { yield 'Just text' => [ 'old_flags' => '', 'old_text' => 'SomeText', 'SomeText' ]; // gzip string below generated with gzdeflate( 'AAAABBAAA' ) yield 'gzip text' => [ 'old_flags' => 'gzip', 'old_text' => "sttttr\002\022\000", 'AAAABBAAA' ]; } /** * @dataProvider provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal */ public function testGetRevisionText_returnsDecompressedTextFieldWhenNotExternal( $flags, $raw, $expected ) { $blobStore = $this->getBlobStore(); $this->assertSame( $expected, $blobStore->expandBlob( $raw, $flags ) ); } public static function provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts() { yield 'Just some text' => [ 'someNonUrlText' ]; yield 'No second URL part' => [ 'someProtocol://' ]; } /** * @dataProvider provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts */ public function testGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts( $text ) { $blobStore = $this->getBlobStore(); $this->assertFalse( $blobStore->expandBlob( $text, [ 'external' ] ) ); } public function testGetRevisionText_external_noOldId() { $this->setService( 'ExternalStoreFactory', new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' ) ); $blobStore = $this->getBlobStore(); $this->assertSame( 'AAAABBAAA', $blobStore->expandBlob( 'ForTesting://cluster1/12345', [ 'external', 'gzip' ] ) ); } private function getWANObjectCache() { return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); } public function testGetRevisionText_external_oldId() { $cache = $this->getWANObjectCache(); $this->setService( 'MainWANObjectCache', $cache ); $this->setService( 'ExternalStoreFactory', new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' ) ); $lb = $this->createMock( LoadBalancer::class ); $access = $this->getServiceContainer()->getExternalStoreAccess(); $blobStore = new SqlBlobStore( $lb, $access, $cache ); $this->assertSame( 'AAAABBAAA', $blobStore->expandBlob( 'ForTesting://cluster1/12345', 'external,gzip', 'tt:7777' ) ); $cacheKey = $cache->makeGlobalKey( 'SqlBlobStore-blob', $lb->getLocalDomainID(), 'tt:7777' ); $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) ); } public function testGetRevisionText_external_oldId_direct_access() { $cache = $this->getWANObjectCache(); $this->setService( 'MainWANObjectCache', $cache ); $this->setService( 'ExternalStoreFactory', new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' ) ); $lb = $this->createMock( LoadBalancer::class ); $access = $this->getServiceContainer()->getExternalStoreAccess(); $blobStore = new SqlBlobStore( $lb, $access, $cache ); $this->assertSame( 'AAAABBAAA', $blobStore->getBlob( 'es:ForTesting://cluster1/12345?flags=external,gzip' ) ); $cacheKey = $cache->makeGlobalKey( 'SqlBlobStore-blob', $lb->getLocalDomainID(), // See ExternalStoreForTesting for the path 'es:ForTesting://cluster1/12345?flags=external,gzip' ); $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) ); } public static function provideTestGetRevisionText_external_oldId_direct_store() { yield 'no compression' => [ false ]; yield 'compression' => [ true ]; } /** * @dataProvider provideTestGetRevisionText_external_oldId_direct_store */ public function testGetRevisionText_external_oldId_direct_store( bool $compression ) { $cache = $this->getWANObjectCache(); $this->setService( 'MainWANObjectCache', $cache ); $this->setService( 'ExternalStoreFactory', new ExternalStoreFactory( [ 'ForTesting' ], [ 'ForTesting://cluster1' ], 'test-id' ) ); $lb = $this->createMock( LoadBalancer::class ); $access = $this->getServiceContainer()->getExternalStoreAccess(); $blobStore = new SqlBlobStore( $lb, $access, $cache ); $blobStore->setUseExternalStore( true ); $blobStore->setCompressBlobs( $compression ); $id = $blobStore->storeBlob( 'A very unique text' ); $this->assertStringStartsWith( 'es:ForTesting://cluster1/', $id ); $this->assertSame( 'A very unique text', $blobStore->getBlob( $id ) ); $cacheKey = $cache->makeGlobalKey( 'SqlBlobStore-blob', $lb->getLocalDomainID(), $id ); $this->assertSame( 'A very unique text', $cache->get( $cacheKey ) ); } }