���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home/rickpfrv/wiki.craftaro.com/tests/phpunit/unit/includes/config/EtcdConfigTest.php
���ѧ٧ѧ�
<?php use Wikimedia\TestingAccessWrapper; class EtcdConfigTest extends MediaWikiUnitTestCase { private function createConfigMock( array $options = [], ?array $methods = null ) { return $this->getMockBuilder( EtcdConfig::class ) ->setConstructorArgs( [ $options + [ 'host' => 'etcd-tcp.example.net', 'directory' => '/', 'timeout' => 0.1, ] ] ) ->onlyMethods( $methods ?? [ 'fetchAllFromEtcd' ] ) ->getMock(); } private static function createEtcdResponse( array $response ) { $baseResponse = [ 'config' => null, 'error' => null, 'retry' => false, 'modifiedIndex' => 0, ]; return array_merge( $baseResponse, $response ); } private function createSimpleConfigMock( array $config, $index = 0 ) { $mock = $this->createConfigMock(); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'config' => $config, 'modifiedIndex' => $index, ] ) ); return $mock; } private function createCallableMock() { return $this ->getMockBuilder( \stdClass::class ) ->addMethods( [ '__invoke' ] ) ->getMock(); } /** * @covers EtcdConfig::has */ public function testHasKnown() { $config = $this->createSimpleConfigMock( [ 'known' => 'value' ] ); $this->assertSame( true, $config->has( 'known' ) ); } /** * @covers EtcdConfig::__construct * @covers EtcdConfig::get */ public function testGetKnown() { $config = $this->createSimpleConfigMock( [ 'known' => 'value' ] ); $this->assertSame( 'value', $config->get( 'known' ) ); } /** * @covers EtcdConfig::has */ public function testHasUnknown() { $config = $this->createSimpleConfigMock( [ 'known' => 'value' ] ); $this->assertSame( false, $config->has( 'unknown' ) ); } /** * @covers EtcdConfig::get */ public function testGetUnknown() { $config = $this->createSimpleConfigMock( [ 'known' => 'value' ] ); $this->expectException( ConfigException::class ); $config->get( 'unknown' ); } /** * @covers EtcdConfig::getModifiedIndex */ public function testGetModifiedIndex() { $config = $this->createSimpleConfigMock( [ 'some' => 'value' ], 123 ); $this->assertSame( 123, $config->getModifiedIndex() ); } /** * @covers EtcdConfig::__construct */ public function testConstructCacheObj() { $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' ) ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, 'modifiedIndex' => 123 ] ); $config = $this->createConfigMock( [ 'cache' => $cache ] ); $this->assertSame( 'from-cache', $config->get( 'known' ) ); } /** * @covers EtcdConfig::__construct */ public function testConstructCacheSpec() { $config = $this->createConfigMock( [ 'cache' => [ 'class' => HashBagOStuff::class ] ] ); $config->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ], ] ) ); $this->assertSame( 'from-fetch', $config->get( 'known' ) ); } /** * Test matrix * * - [x] Cache miss * Result: Fetched value * > cache miss | gets lock | backend succeeds * * - [x] Cache miss with backend error * Result: ConfigException * > cache miss | gets lock | backend error (no retry) * * - [x] Cache hit after retry * Result: Cached value (populated by process holding lock) * > cache miss | no lock | cache retry * * - [x] Cache hit * Result: Cached value * > cache hit * * - [x] Process cache hit * Result: Cached value * > process cache hit * * - [x] Cache expired * Result: Fetched value * > cache expired | gets lock | backend succeeds * * - [x] Cache expired with backend failure * Result: Cached value (stale) * > cache expired | gets lock | backend fails (allows retry) * * - [x] Cache expired and no lock * Result: Cached value (stale) * > cache expired | no lock * * Other notable scenarios: * * - [ ] Cache miss with backend retry * Result: Fetched value * > cache expired | gets lock | backend failure (allows retry) */ /** * @covers EtcdConfig::load */ public function testLoadCacheMiss() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); // .. misses cache $cache->expects( $this->once() )->method( 'get' ) ->willReturn( false ); // .. gets lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( true ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) ); $this->assertSame( 'from-fetch', $mock->get( 'known' ) ); } /** * @covers EtcdConfig::load */ public function testLoadCacheMissBackendError() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); // .. misses cache $cache->expects( $this->once() )->method( 'get' ) ->willReturn( false ); // .. gets lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( true ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) ); $this->expectException( ConfigException::class ); $mock->get( 'key' ); } /** * @covers EtcdConfig::load */ public function testLoadCacheMissWithoutLock() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->exactly( 2 ) )->method( 'get' ) ->will( $this->onConsecutiveCalls( // .. misses cache first time false, // .. hits cache on retry [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, 'modifiedIndex' => 123 ] ) ); // .. misses lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( false ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' ); $this->assertSame( 'from-cache', $mock->get( 'known' ) ); } /** * @covers EtcdConfig::load */ public function testLoadCacheHit() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' ) // .. hits cache ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, 'modifiedIndex' => 0, ] ); $cache->expects( $this->never() )->method( 'lock' ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' ); $this->assertSame( 'from-cache', $mock->get( 'known' ) ); } /** * @covers EtcdConfig::load */ public function testLoadProcessCacheHit() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' ) // .. hits cache ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, 'modifiedIndex' => 0, ] ); $cache->expects( $this->never() )->method( 'lock' ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' ); $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Cache hit' ); $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Process cache hit' ); } /** * @covers EtcdConfig::load */ public function testLoadCacheExpiredLockFetchSucceeded() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' )->willReturn( // .. stale cache [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, 'modifiedIndex' => 0, ] ); // .. gets lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( true ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) ); $this->assertSame( 'from-fetch', $mock->get( 'known' ) ); } /** * @covers EtcdConfig::load */ public function testLoadCacheExpiredLockFetchFails() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' )->willReturn( // .. stale cache [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, 'modifiedIndex' => 0, ] ); // .. gets lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( true ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) ); $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) ); } /** * @covers EtcdConfig::load */ public function testLoadCacheExpiredNoLock() { // Create cache mock $cache = $this->getMockBuilder( HashBagOStuff::class ) ->onlyMethods( [ 'get', 'lock' ] ) ->getMock(); $cache->expects( $this->once() )->method( 'get' ) // .. hits cache (expired value) ->willReturn( [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, 'modifiedIndex' => 0, ] ); // .. misses lock $cache->expects( $this->once() )->method( 'lock' ) ->willReturn( false ); // Create config mock $mock = $this->createConfigMock( [ 'cache' => $cache, ] ); $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' ); $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) ); } public static function provideFetchFromServer() { return [ '200 OK - Success' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', 'value' => json_encode( [ 'val' => true ] ), 'modifiedIndex' => 123 ], ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'config' => [ 'foo' => true ], // data 'modifiedIndex' => 123 ] ), ], '200 OK - Empty dir' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', 'value' => json_encode( [ 'val' => true ] ), 'modifiedIndex' => 123 ], [ 'key' => '/example/sub', 'dir' => true, 'modifiedIndex' => 234, 'nodes' => [], ], [ 'key' => '/example/bar', 'value' => json_encode( [ 'val' => false ] ), 'modifiedIndex' => 125 ], ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'config' => [ 'foo' => true, 'bar' => false ], // data 'modifiedIndex' => 125 // largest modified index ] ), ], '200 OK - Recursive' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/a', 'dir' => true, 'modifiedIndex' => 124, 'nodes' => [ [ 'key' => 'b', 'value' => json_encode( [ 'val' => true ] ), 'modifiedIndex' => 123, ], [ 'key' => 'c', 'value' => json_encode( [ 'val' => false ] ), 'modifiedIndex' => 123, ], ], ], ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'config' => [ 'a/b' => true, 'a/c' => false ], // data 'modifiedIndex' => 123 // largest modified index ] ), ], '200 OK - Missing nodes at second level' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/a', 'dir' => true, 'modifiedIndex' => 0, ], ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.", ] ), ], '200 OK - Directory with non-array "nodes" key' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/a', 'dir' => true, 'nodes' => 'not an array' ], ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.", ] ), ], '200 OK - Correctly encoded garbage response' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'foo' => 'bar' ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'error' => "Unexpected JSON response: Missing or invalid node at top level.", ] ), ], '200 OK - Bad value' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', 'value' => ';"broken{value', 'modifiedIndex' => 123, ] ] ] ] ), 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'error' => "Failed to parse value for 'foo'.", ] ), ], '200 OK - Empty node list' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], 'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}', 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'config' => [], // data ] ), ], '200 OK - Invalid JSON' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [ 'content-length' => 0 ], 'body' => '', 'error' => '(curl error: no status set)', ], 'expect' => self::createEtcdResponse( [ 'error' => "Error unserializing JSON response.", ] ), ], '404 Not Found' => [ 'http' => [ 'code' => 404, 'reason' => 'Not Found', 'headers' => [ 'content-length' => 0 ], 'body' => '', 'error' => '', ], 'expect' => self::createEtcdResponse( [ 'error' => 'HTTP 404 (Not Found)', ] ), ], '400 Bad Request - custom error' => [ 'http' => [ 'code' => 400, 'reason' => 'Bad Request', 'headers' => [ 'content-length' => 0 ], 'body' => '', 'error' => 'No good reason', ], 'expect' => self::createEtcdResponse( [ 'error' => 'No good reason', 'retry' => true, // retry ] ), ], ]; } /** * @covers EtcdConfig::fetchAllFromEtcdServer * @covers EtcdConfig::unserialize * @covers EtcdConfig::parseResponse * @covers EtcdConfig::parseDirectory * @covers EtcdConfigParseError * @dataProvider provideFetchFromServer */ public function testFetchFromServer( array $httpResponse, array $expected ) { $http = $this->createMock( MultiHttpClient::class ); $http->expects( $this->once() )->method( 'run' ) ->willReturn( array_values( $httpResponse ) ); $conf = $this->createMock( EtcdConfig::class ); // Access for protected member and method $conf = TestingAccessWrapper::newFromObject( $conf ); $conf->http = $http; $this->assertSame( $expected, $conf->fetchAllFromEtcdServer( 'etcd-tcp.example.net' ) ); } /** * @covers EtcdConfig::fetchAllFromEtcdServer */ public function testFetchFromServerWithoutPort() { $conf = $this->createMock( EtcdConfig::class ); $http = $this->createMock( MultiHttpClient::class ); $conf = TestingAccessWrapper::newFromObject( $conf ); $conf->protocol = 'https'; $conf->http = $http; $http ->expects( $this->once() ) ->method( 'run' ) ->with( $this->logicalAnd( $this->arrayHasKey( 'url' ), $this->callback( function ( $request ) { $this->assertStringStartsWith( 'https://etcd.example/', $request['url'] ); return true; } ) ) ); $conf->fetchAllFromEtcdServer( 'etcd.example' ); } /** * @covers EtcdConfig::fetchAllFromEtcdServer */ public function testFetchFromServerWithPort() { $conf = $this->createMock( EtcdConfig::class ); $http = $this->createMock( MultiHttpClient::class ); $conf = TestingAccessWrapper::newFromObject( $conf ); $conf->protocol = 'https'; $conf->http = $http; $http ->expects( $this->once() ) ->method( 'run' ) ->with( $this->logicalAnd( $this->arrayHasKey( 'url' ), $this->callback( function ( $request ) { $this->assertStringStartsWith( 'https://etcd.example:4001/', $request['url'] ); return true; } ) ) ); $conf->fetchAllFromEtcdServer( 'etcd.example', 4001 ); } /** * @covers EtcdConfig::fetchAllFromEtcd */ public function testServiceDiscovery() { $conf = $this->createConfigMock( [ 'host' => 'an.example' ], [ 'fetchAllFromEtcdServer' ] ); $conf = TestingAccessWrapper::newFromObject( $conf ); $conf->dsd = TestingAccessWrapper::newFromObject( $conf->dsd ); $conf->dsd->resolver = $this->createCallableMock(); $conf->dsd->resolver ->expects( $this->once() ) ->method( '__invoke' ) ->with( '_etcd._tcp.an.example' ) ->willReturn( [ [ 'target' => 'etcd-target.an.example', 'port' => '2379', 'pri' => '1', 'weight' => '1', ], ] ); $conf->expects( $this->once() ) ->method( 'fetchAllFromEtcdServer' ) ->with( 'etcd-target.an.example', 2379 ) ->willReturn( self::createEtcdResponse( [ 'foo' => true ] ) ); $conf->fetchAllFromEtcd(); } /** * @covers EtcdConfig::fetchAllFromEtcd */ public function testServiceDiscoverySrvRecordAsHost() { $conf = $this->createConfigMock( [ 'host' => '_etcd-client-ssl._tcp.an.example' ], [ 'fetchAllFromEtcdServer' ] ); $conf = TestingAccessWrapper::newFromObject( $conf ); $conf->dsd = TestingAccessWrapper::newFromObject( $conf->dsd ); $conf->dsd->resolver = $this->createCallableMock(); $conf->dsd->resolver ->expects( $this->once() ) ->method( '__invoke' ) ->with( '_etcd-client-ssl._tcp.an.example' ) ->willReturn( [ [ 'target' => 'etcd-target.an.example', 'port' => '2379', 'pri' => '1', 'weight' => '1', ], ] ); $conf->expects( $this->once() ) ->method( 'fetchAllFromEtcdServer' ) ->with( 'etcd-target.an.example', 2379 ) ->willReturn( self::createEtcdResponse( [ 'foo' => true ] ) ); $conf->fetchAllFromEtcd(); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0.39 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�