362 lines
9.3 KiB
PHP
362 lines
9.3 KiB
PHP
<?php
|
|
|
|
class EtcConfigTest extends PHPUnit_Framework_TestCase {
|
|
|
|
private function createConfigMock( array $options = [] ) {
|
|
return $this->getMockBuilder( EtcdConfig::class )
|
|
->setConstructorArgs( [ $options + [
|
|
'host' => 'etcd-tcp.example.net',
|
|
'directory' => '/',
|
|
'timeout' => 0.1,
|
|
] ] )
|
|
->setMethods( [ 'fetchAllFromEtcd' ] )
|
|
->getMock();
|
|
}
|
|
|
|
private function createSimpleConfigMock( array $config ) {
|
|
$mock = $this->createConfigMock();
|
|
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
|
|
->willReturn( [
|
|
$config,
|
|
null, // error
|
|
false // retry?
|
|
] );
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @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->setExpectedException( ConfigException::class );
|
|
$config->get( 'unknown' );
|
|
}
|
|
|
|
/**
|
|
* @covers EtcdConfig::__construct
|
|
*/
|
|
public function testConstructCacheObj() {
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->setMethods( [ 'get' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )
|
|
->willReturn( [
|
|
'config' => [ 'known' => 'from-cache' ],
|
|
'expires' => INF,
|
|
] );
|
|
$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( [
|
|
[ 'known' => 'from-fetch' ],
|
|
null, // error
|
|
false // retry?
|
|
] );
|
|
|
|
$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 )
|
|
->setMethods( [ '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( [ [ 'known' => 'from-fetch' ], null, false ] );
|
|
|
|
$this->assertSame( 'from-fetch', $mock->get( 'known' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers EtcdConfig::load
|
|
*/
|
|
public function testLoadCacheMissBackendError() {
|
|
// Create cache mock
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->setMethods( [ '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( [ null, 'Fake error', false ] );
|
|
|
|
$this->setExpectedException( ConfigException::class );
|
|
$mock->get( 'key' );
|
|
}
|
|
|
|
/**
|
|
* @covers EtcdConfig::load
|
|
*/
|
|
public function testLoadCacheMissWithoutLock() {
|
|
// Create cache mock
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->setMethods( [ '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,
|
|
]
|
|
) );
|
|
// .. 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 )
|
|
->setMethods( [ 'get', 'lock' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )
|
|
// .. hits cache
|
|
->willReturn( [
|
|
'config' => [ 'known' => 'from-cache' ],
|
|
'expires' => INF,
|
|
] );
|
|
$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 )
|
|
->setMethods( [ 'get', 'lock' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )
|
|
// .. hits cache
|
|
->willReturn( [
|
|
'config' => [ 'known' => 'from-cache' ],
|
|
'expires' => INF,
|
|
] );
|
|
$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 )
|
|
->setMethods( [ 'get', 'lock' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )->willReturn(
|
|
// .. stale cache
|
|
[
|
|
'config' => [ 'known' => 'from-cache-expired' ],
|
|
'expires' => -INF,
|
|
]
|
|
);
|
|
// .. 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( [ [ 'known' => 'from-fetch' ], null, false ] );
|
|
|
|
$this->assertSame( 'from-fetch', $mock->get( 'known' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers EtcdConfig::load
|
|
*/
|
|
public function testLoadCacheExpiredLockFetchFails() {
|
|
// Create cache mock
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->setMethods( [ 'get', 'lock' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )->willReturn(
|
|
// .. stale cache
|
|
[
|
|
'config' => [ 'known' => 'from-cache-expired' ],
|
|
'expires' => -INF,
|
|
]
|
|
);
|
|
// .. 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( [ null, 'Fake failure', true ] );
|
|
|
|
$this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers EtcdConfig::load
|
|
*/
|
|
public function testLoadCacheExpiredNoLock() {
|
|
// Create cache mock
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->setMethods( [ 'get', 'lock' ] )
|
|
->getMock();
|
|
$cache->expects( $this->once() )->method( 'get' )
|
|
// .. hits cache (expired value)
|
|
->willReturn( [
|
|
'config' => [ 'known' => 'from-cache-expired' ],
|
|
'expires' => -INF,
|
|
] );
|
|
// .. 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' ) );
|
|
}
|
|
}
|