Krinkle has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/350508 )

Change subject: [WIP] Add unit test for EtcdConfig
......................................................................

[WIP] Add unit test for EtcdConfig

Change-Id: I6e647985b98724b5448831a6fae1b2d2521fd363
---
M includes/config/EtcdConfig.php
A tests/phpunit/includes/config/EtcdConfigTest.php
M tests/phpunit/suite.xml
3 files changed, 266 insertions(+), 5 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/08/350508/1

diff --git a/includes/config/EtcdConfig.php b/includes/config/EtcdConfig.php
index d3fbd65..09d1761 100644
--- a/includes/config/EtcdConfig.php
+++ b/includes/config/EtcdConfig.php
@@ -88,7 +88,7 @@
                $this->timeout = $params['timeout'];
 
                if ( !isset( $params['cache'] ) ) {
-                       $this->srvCache = new HashBagOStuff( [] );
+                       $this->srvCache = new HashBagOStuff();
                } elseif ( $params['cache'] instanceof BagOStuff ) {
                        $this->srvCache = $params['cache'];
                } else {
@@ -122,6 +122,37 @@
                return $this->procCache['config'][$name];
        }
 
+       /**
+        * Scenarios:
+        * - Cache exists:
+        *   - If not expired:
+        *     - Use the cached value. (fresh cache)
+        *   - If expired:
+        *     - If another process already has the lock:
+        *       - Use the cached value. (stale cache)
+        *     - If we get the lock:
+        *       - Backend fetch succeeds:
+        *         - Use the fetched value. (fresh fetch)
+        *       - Backend fetch fails:
+        *         - Throws!
+        *       - Backend fetch times out:
+        *         - Retries.
+        *       - Retry loop times out:
+        *         - Throws!
+        * - Cache is empty:
+        *     - If another process already has the lock:
+        *       - Retry reading the cache.
+        *     - If we get the lock:
+        *       - Backend fetch succeeds:
+        *         - Use the fetched value. (fresh fetch)
+        *       - Backend fetch fails:
+        *         - Throws!
+        *       - Backend fetch times out:
+        *         - Retries.
+        *       - Retry loop times out:
+        *         - Throws!
+        * @throws ConfigException
+        */
        private function load() {
                if ( $this->procCache !== null ) {
                        return; // already loaded
diff --git a/tests/phpunit/includes/config/EtcdConfigTest.php 
b/tests/phpunit/includes/config/EtcdConfigTest.php
new file mode 100644
index 0000000..f526693
--- /dev/null
+++ b/tests/phpunit/includes/config/EtcdConfigTest.php
@@ -0,0 +1,233 @@
+<?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
+        * @covers EtcdConfig::get
+        */
+       public function testKnownKey() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+
+               $this->assertSame(
+                       true,
+                       $config->has( 'known' ),
+                       'Has key'
+               );
+
+               $this->assertSame(
+                       'value',
+                       $config->get( 'known' ),
+                       'Get key'
+               );
+       }
+
+       /**
+        * @covers EtcdConfig::has
+        */
+       public function testHasUnknown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+
+               $this->assertSame(
+                       false,
+                       $config->has( 'unknown' ),
+                       'Has key'
+               );
+       }
+
+       /**
+        * @covers EtcdConfig::get
+        */
+       public function testGetUnknown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+
+               $this->setExpectedException( ConfigException::class );
+               $config->get( 'unknown' );
+       }
+
+       public static function provideLoadScenarios() {
+               // Format:
+               // - cache: true, false, or "expired"
+               // - lock: true, or false
+               // - backend: "success", "fail", "retry"
+               // - retry: true, or false (Whether a retry should succeed)
+               // - expected:
+               //   - "fresh" Fetched from a backend.
+               //   - "fresh-retried" Fetched from a backend after retry.
+               //   - "cache" Fetched from cache (not expired).
+               //   - "stale-cache" Fetched from cache but known stale.
+               return [
+                       // cache miss, gets lock
+                       [ [
+                               'cache' => false, 'lock' => true, 'backend' => 
'success',
+                               'expected' => 'fresh',
+                       ] ],
+                       [ [
+                               'cache' => false, 'lock' => true, 'backend' => 
'fail',
+                               'expected' => false,
+                       ] ],
+                       [ [
+                               'cache' => false, 'lock' => true, 'backend' => 
'retry', 'retry' => true,
+                               'expected' => 'fresh-retried',
+                       ] ],
+                       [ [
+                               'cache' => false, 'lock' => true, 'backend' => 
'retry', 'retry' => false,
+                               'expected' => false,
+                       ] ],
+                       // cache miss, doesn't get lock
+                       [ [
+                               'cache' => false, 'lock' => false, 'retry' => 
true,
+                               // Populated by another process after retry
+                               'expected' => 'cache',
+                       ] ],
+                       [ [
+                               'cache' => false, 'lock' => false, 'retry' => 
false,
+                               'expected' => false,
+                       ] ],
+                       // cache hit
+                       [ [
+                               'cache' => true,
+                               'expected' => 'cache',
+                       ] ],
+                       // cache expired, gets lock
+                       [ [
+                               'cache' => 'expired', 'lock' => true, 'backend' 
=> 'success',
+                               'expected' => 'fresh',
+                       ] ],
+                       [ [
+                               'cache' => 'expired', 'lock' => true, 'backend' 
=> 'fail',
+                               'expected' => false,
+                       ] ],
+                       // cache expired, doesn't get lock
+                       [ [
+                               'cache' => 'expired', 'lock' => false,
+                               'expected' => 'cache-stale',
+                       ] ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideLoadScenarios
+        */
+       public function testLoadScenarios( array $case ) {
+               $case += [ 'lock' => false, 'backend' => null ];
+
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setConstructorArgs( [] )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cacheKey = $cache->makeKey( 'variable', sha1( '/' ) );
+
+               $cacheValue = [
+                       'config' => [ 'key' => 'cache' ],
+                       'expires' => time() * 2,
+               ];
+               $cacheStaleValue = [
+                       'config' => [ 'key' => 'cache-stale' ],
+                       'expires' => 1,
+               ];
+
+               // Prepare lock
+               $cache->expects( $this->any() )
+                       ->method( 'lock' )
+                       ->willReturn( $case['lock'] );
+
+
+               // Prepare cache
+               if ( $case['cache'] === true ) {
+                       $firstCache = $cacheValue;
+               } elseif ( $case['cache'] === 'expired' ) {
+                       $firstCache = $cacheStaleValue;
+               } else {
+                       $firstCache = false;
+               }
+               if ( $case['lock'] === false && $case['cache'] === false && 
$case['retry'] === true ) {
+                       $cache->expects( $this->exactly( 2 ) )
+                               ->method( 'get' )
+                               ->will( $this->onConsecutiveCalls(
+                                       $firstCache,
+                                       $cacheValue
+                               ) );
+               } else {
+                       $cache->expects( $this->any() )
+                               ->method( 'get' )
+                               ->willReturn( $firstCache );
+               }
+
+               // Create mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+
+               // Set up mock backend
+               if ( $case['backend'] === null ) {
+                       $mock->expects( $this->never() )
+                               ->method( 'fetchAllFromEtcd' );
+               } else {
+                       if ( $case['backend'] === 'success' ) {
+                               $fetch = [ [ 'key' => 'fresh' ], null, false ];
+                               $mock->expects( $this->once() )
+                                       ->method( 'fetchAllFromEtcd' )
+                                       ->willReturn( $fetch );
+                       } elseif ( $case['backend'] === 'fail' ) {
+                               $fetch = [ null, 'Fake failure', false ];
+                               $mock->expects( $this->once() )
+                                       ->method( 'fetchAllFromEtcd' )
+                                       ->willReturn( $fetch );
+                       } elseif ( $case['backend'] === 'retry' ) {
+                               $firstFetch = [ null, 'Fake retryable failure', 
true ];
+                               if ( $case['retry'] === true ) {
+                                       $secondFetch = [ [ 'key' => 
'fresh-retried' ], null, false ];
+                               } else {
+                                       $secondFetch = $firstFetch;
+                               }
+                               $mock->expects( $this->atLeastOnce() )
+                                       ->method( 'fetchAllFromEtcd' )
+                                       ->will( $this->onConsecutiveCalls(
+                                               $firstFetch,
+                                               $secondFetch
+                                       ) );
+                       }
+               }
+
+               if ( $case['expected'] == false ) {
+                       $this->setExpectedException( ConfigException::class );
+                       $mock->get( 'key' );
+               } else {
+                       $this->assertSame(
+                               $case['expected'],
+                               $mock->get( 'key' ),
+                               'Get key'
+                       );
+               }
+       }
+}
diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml
index 7babcac..63c2568 100644
--- a/tests/phpunit/suite.xml
+++ b/tests/phpunit/suite.xml
@@ -64,10 +64,7 @@
        </groups>
        <filter>
                <whitelist addUncoveredFilesFromWhitelist="true">
-                       <directory suffix=".php">../../includes</directory>
-                       <directory suffix=".php">../../languages</directory>
-                       <directory suffix=".php">../../maintenance</directory>
-                       <directory suffix=".php">../../skins</directory>
+                       <directory 
suffix=".php">../../includes/config</directory>
                </whitelist>
        </filter>
 </phpunit>

-- 
To view, visit https://gerrit.wikimedia.org/r/350508
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I6e647985b98724b5448831a6fae1b2d2521fd363
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to