MediaWiki  1.30.0
EtcdConfigTest.php
Go to the documentation of this file.
1 <?php
2 
3 use Wikimedia\TestingAccessWrapper;
4 
5 class EtcConfigTest extends PHPUnit_Framework_TestCase {
6 
7  private function createConfigMock( array $options = [] ) {
8  return $this->getMockBuilder( EtcdConfig::class )
9  ->setConstructorArgs( [ $options + [
10  'host' => 'etcd-tcp.example.net',
11  'directory' => '/',
12  'timeout' => 0.1,
13  ] ] )
14  ->setMethods( [ 'fetchAllFromEtcd' ] )
15  ->getMock();
16  }
17 
18  private function createSimpleConfigMock( array $config ) {
19  $mock = $this->createConfigMock();
20  $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
21  ->willReturn( [
22  $config,
23  null, // error
24  false // retry?
25  ] );
26  return $mock;
27  }
28 
32  public function testHasKnown() {
33  $config = $this->createSimpleConfigMock( [
34  'known' => 'value'
35  ] );
36  $this->assertSame( true, $config->has( 'known' ) );
37  }
38 
43  public function testGetKnown() {
44  $config = $this->createSimpleConfigMock( [
45  'known' => 'value'
46  ] );
47  $this->assertSame( 'value', $config->get( 'known' ) );
48  }
49 
53  public function testHasUnknown() {
54  $config = $this->createSimpleConfigMock( [
55  'known' => 'value'
56  ] );
57  $this->assertSame( false, $config->has( 'unknown' ) );
58  }
59 
63  public function testGetUnknown() {
64  $config = $this->createSimpleConfigMock( [
65  'known' => 'value'
66  ] );
67  $this->setExpectedException( ConfigException::class );
68  $config->get( 'unknown' );
69  }
70 
74  public function testConstructCacheObj() {
75  $cache = $this->getMockBuilder( HashBagOStuff::class )
76  ->setMethods( [ 'get' ] )
77  ->getMock();
78  $cache->expects( $this->once() )->method( 'get' )
79  ->willReturn( [
80  'config' => [ 'known' => 'from-cache' ],
81  'expires' => INF,
82  ] );
83  $config = $this->createConfigMock( [ 'cache' => $cache ] );
84 
85  $this->assertSame( 'from-cache', $config->get( 'known' ) );
86  }
87 
91  public function testConstructCacheSpec() {
92  $config = $this->createConfigMock( [ 'cache' => [
93  'class' => HashBagOStuff::class
94  ] ] );
95  $config->expects( $this->once() )->method( 'fetchAllFromEtcd' )
96  ->willReturn( [
97  [ 'known' => 'from-fetch' ],
98  null, // error
99  false // retry?
100  ] );
101 
102  $this->assertSame( 'from-fetch', $config->get( 'known' ) );
103  }
104 
150  public function testLoadCacheMiss() {
151  // Create cache mock
152  $cache = $this->getMockBuilder( HashBagOStuff::class )
153  ->setMethods( [ 'get', 'lock' ] )
154  ->getMock();
155  // .. misses cache
156  $cache->expects( $this->once() )->method( 'get' )
157  ->willReturn( false );
158  // .. gets lock
159  $cache->expects( $this->once() )->method( 'lock' )
160  ->willReturn( true );
161 
162  // Create config mock
163  $mock = $this->createConfigMock( [
164  'cache' => $cache,
165  ] );
166  $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
167  ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
168 
169  $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
170  }
171 
175  public function testLoadCacheMissBackendError() {
176  // Create cache mock
177  $cache = $this->getMockBuilder( HashBagOStuff::class )
178  ->setMethods( [ 'get', 'lock' ] )
179  ->getMock();
180  // .. misses cache
181  $cache->expects( $this->once() )->method( 'get' )
182  ->willReturn( false );
183  // .. gets lock
184  $cache->expects( $this->once() )->method( 'lock' )
185  ->willReturn( true );
186 
187  // Create config mock
188  $mock = $this->createConfigMock( [
189  'cache' => $cache,
190  ] );
191  $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
192  ->willReturn( [ null, 'Fake error', false ] );
193 
194  $this->setExpectedException( ConfigException::class );
195  $mock->get( 'key' );
196  }
197 
201  public function testLoadCacheMissWithoutLock() {
202  // Create cache mock
203  $cache = $this->getMockBuilder( HashBagOStuff::class )
204  ->setMethods( [ 'get', 'lock' ] )
205  ->getMock();
206  $cache->expects( $this->exactly( 2 ) )->method( 'get' )
207  ->will( $this->onConsecutiveCalls(
208  // .. misses cache first time
209  false,
210  // .. hits cache on retry
211  [
212  'config' => [ 'known' => 'from-cache' ],
213  'expires' => INF,
214  ]
215  ) );
216  // .. misses lock
217  $cache->expects( $this->once() )->method( 'lock' )
218  ->willReturn( false );
219 
220  // Create config mock
221  $mock = $this->createConfigMock( [
222  'cache' => $cache,
223  ] );
224  $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
225 
226  $this->assertSame( 'from-cache', $mock->get( 'known' ) );
227  }
228 
232  public function testLoadCacheHit() {
233  // Create cache mock
234  $cache = $this->getMockBuilder( HashBagOStuff::class )
235  ->setMethods( [ 'get', 'lock' ] )
236  ->getMock();
237  $cache->expects( $this->once() )->method( 'get' )
238  // .. hits cache
239  ->willReturn( [
240  'config' => [ 'known' => 'from-cache' ],
241  'expires' => INF,
242  ] );
243  $cache->expects( $this->never() )->method( 'lock' );
244 
245  // Create config mock
246  $mock = $this->createConfigMock( [
247  'cache' => $cache,
248  ] );
249  $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
250 
251  $this->assertSame( 'from-cache', $mock->get( 'known' ) );
252  }
253 
257  public function testLoadProcessCacheHit() {
258  // Create cache mock
259  $cache = $this->getMockBuilder( HashBagOStuff::class )
260  ->setMethods( [ 'get', 'lock' ] )
261  ->getMock();
262  $cache->expects( $this->once() )->method( 'get' )
263  // .. hits cache
264  ->willReturn( [
265  'config' => [ 'known' => 'from-cache' ],
266  'expires' => INF,
267  ] );
268  $cache->expects( $this->never() )->method( 'lock' );
269 
270  // Create config mock
271  $mock = $this->createConfigMock( [
272  'cache' => $cache,
273  ] );
274  $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
275 
276  $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Cache hit' );
277  $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Process cache hit' );
278  }
279 
284  // Create cache mock
285  $cache = $this->getMockBuilder( HashBagOStuff::class )
286  ->setMethods( [ 'get', 'lock' ] )
287  ->getMock();
288  $cache->expects( $this->once() )->method( 'get' )->willReturn(
289  // .. stale cache
290  [
291  'config' => [ 'known' => 'from-cache-expired' ],
292  'expires' => -INF,
293  ]
294  );
295  // .. gets lock
296  $cache->expects( $this->once() )->method( 'lock' )
297  ->willReturn( true );
298 
299  // Create config mock
300  $mock = $this->createConfigMock( [
301  'cache' => $cache,
302  ] );
303  $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
304  ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
305 
306  $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
307  }
308 
313  // Create cache mock
314  $cache = $this->getMockBuilder( HashBagOStuff::class )
315  ->setMethods( [ 'get', 'lock' ] )
316  ->getMock();
317  $cache->expects( $this->once() )->method( 'get' )->willReturn(
318  // .. stale cache
319  [
320  'config' => [ 'known' => 'from-cache-expired' ],
321  'expires' => -INF,
322  ]
323  );
324  // .. gets lock
325  $cache->expects( $this->once() )->method( 'lock' )
326  ->willReturn( true );
327 
328  // Create config mock
329  $mock = $this->createConfigMock( [
330  'cache' => $cache,
331  ] );
332  $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
333  ->willReturn( [ null, 'Fake failure', true ] );
334 
335  $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
336  }
337 
341  public function testLoadCacheExpiredNoLock() {
342  // Create cache mock
343  $cache = $this->getMockBuilder( HashBagOStuff::class )
344  ->setMethods( [ 'get', 'lock' ] )
345  ->getMock();
346  $cache->expects( $this->once() )->method( 'get' )
347  // .. hits cache (expired value)
348  ->willReturn( [
349  'config' => [ 'known' => 'from-cache-expired' ],
350  'expires' => -INF,
351  ] );
352  // .. misses lock
353  $cache->expects( $this->once() )->method( 'lock' )
354  ->willReturn( false );
355 
356  // Create config mock
357  $mock = $this->createConfigMock( [
358  'cache' => $cache,
359  ] );
360  $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
361 
362  $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
363  }
364 
365  public static function provideFetchFromServer() {
366  return [
367  '200 OK - Success' => [
368  'http' => [
369  'code' => 200,
370  'reason' => 'OK',
371  'headers' => [],
372  'body' => json_encode( [ 'node' => [ 'nodes' => [
373  [
374  'key' => '/example/foo',
375  'value' => json_encode( [ 'val' => true ] )
376  ],
377  ] ] ] ),
378  'error' => '',
379  ],
380  'expect' => [
381  [ 'foo' => true ], // data
382  null,
383  false // retry
384  ],
385  ],
386  '200 OK - Empty dir' => [
387  'http' => [
388  'code' => 200,
389  'reason' => 'OK',
390  'headers' => [],
391  'body' => json_encode( [ 'node' => [ 'nodes' => [
392  [
393  'key' => '/example/foo',
394  'value' => json_encode( [ 'val' => true ] )
395  ],
396  [
397  'key' => '/example/sub',
398  'dir' => true,
399  'nodes' => [],
400  ],
401  [
402  'key' => '/example/bar',
403  'value' => json_encode( [ 'val' => false ] )
404  ],
405  ] ] ] ),
406  'error' => '',
407  ],
408  'expect' => [
409  [ 'foo' => true, 'bar' => false ], // data
410  null,
411  false // retry
412  ],
413  ],
414  '200 OK - Recursive' => [
415  'http' => [
416  'code' => 200,
417  'reason' => 'OK',
418  'headers' => [],
419  'body' => json_encode( [ 'node' => [ 'nodes' => [
420  [
421  'key' => '/example/a',
422  'dir' => true,
423  'nodes' => [
424  [
425  'key' => 'b',
426  'value' => json_encode( [ 'val' => true ] ),
427  ],
428  [
429  'key' => 'c',
430  'value' => json_encode( [ 'val' => false ] ),
431  ],
432  ],
433  ],
434  ] ] ] ),
435  'error' => '',
436  ],
437  'expect' => [
438  [ 'a/b' => true, 'a/c' => false ], // data
439  null,
440  false // retry
441  ],
442  ],
443  '200 OK - Missing nodes at second level' => [
444  'http' => [
445  'code' => 200,
446  'reason' => 'OK',
447  'headers' => [],
448  'body' => json_encode( [ 'node' => [ 'nodes' => [
449  [
450  'key' => '/example/a',
451  'dir' => true,
452  ],
453  ] ] ] ),
454  'error' => '',
455  ],
456  'expect' => [
457  null,
458  "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
459  false // retry
460  ],
461  ],
462  '200 OK - Correctly encoded garbage response' => [
463  'http' => [
464  'code' => 200,
465  'reason' => 'OK',
466  'headers' => [],
467  'body' => json_encode( [ 'foo' => 'bar' ] ),
468  'error' => '',
469  ],
470  'expect' => [
471  null,
472  "Unexpected JSON response: Missing or invalid node at top level.",
473  false // retry
474  ],
475  ],
476  '200 OK - Bad value' => [
477  'http' => [
478  'code' => 200,
479  'reason' => 'OK',
480  'headers' => [],
481  'body' => json_encode( [ 'node' => [ 'nodes' => [
482  [
483  'key' => '/example/foo',
484  'value' => ';"broken{value'
485  ]
486  ] ] ] ),
487  'error' => '',
488  ],
489  'expect' => [
490  null, // data
491  "Failed to parse value for 'foo'.",
492  false // retry
493  ],
494  ],
495  '200 OK - Empty node list' => [
496  'http' => [
497  'code' => 200,
498  'reason' => 'OK',
499  'headers' => [],
500  'body' => '{"node":{"nodes":[]}}',
501  'error' => '',
502  ],
503  'expect' => [
504  [], // data
505  null,
506  false // retry
507  ],
508  ],
509  '200 OK - Invalid JSON' => [
510  'http' => [
511  'code' => 200,
512  'reason' => 'OK',
513  'headers' => [ 'content-length' => 0 ],
514  'body' => '',
515  'error' => '(curl error: no status set)',
516  ],
517  'expect' => [
518  null, // data
519  "Error unserializing JSON response.",
520  false // retry
521  ],
522  ],
523  '404 Not Found' => [
524  'http' => [
525  'code' => 404,
526  'reason' => 'Not Found',
527  'headers' => [ 'content-length' => 0 ],
528  'body' => '',
529  'error' => '',
530  ],
531  'expect' => [
532  null, // data
533  'HTTP 404 (Not Found)',
534  false // retry
535  ],
536  ],
537  '400 Bad Request - custom error' => [
538  'http' => [
539  'code' => 400,
540  'reason' => 'Bad Request',
541  'headers' => [ 'content-length' => 0 ],
542  'body' => '',
543  'error' => 'No good reason',
544  ],
545  'expect' => [
546  null, // data
547  'No good reason',
548  true // retry
549  ],
550  ],
551  ];
552  }
553 
562  public function testFetchFromServer( array $httpResponse, array $expected ) {
563  $http = $this->getMockBuilder( MultiHttpClient::class )
564  ->disableOriginalConstructor()
565  ->getMock();
566  $http->expects( $this->once() )->method( 'run' )
567  ->willReturn( array_values( $httpResponse ) );
568 
569  $conf = $this->getMockBuilder( EtcdConfig::class )
570  ->disableOriginalConstructor()
571  ->getMock();
572  // Access for protected member and method
573  $conf = TestingAccessWrapper::newFromObject( $conf );
574  $conf->http = $http;
575 
576  $this->assertSame(
577  $expected,
578  $conf->fetchAllFromEtcdServer( 'etcd-tcp.example.net' )
579  );
580  }
581 }
EtcConfigTest\provideFetchFromServer
static provideFetchFromServer()
Definition: EtcdConfigTest.php:365
EtcConfigTest\testFetchFromServer
testFetchFromServer(array $httpResponse, array $expected)
EtcdConfig::fetchAllFromEtcdServer EtcdConfig::unserialize EtcdConfig::parseResponse EtcdConfig::pars...
Definition: EtcdConfigTest.php:562
EtcConfigTest
Definition: EtcdConfigTest.php:5
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
EtcConfigTest\testLoadCacheExpiredLockFetchSucceeded
testLoadCacheExpiredLockFetchSucceeded()
EtcdConfig::load.
Definition: EtcdConfigTest.php:283
EtcConfigTest\testLoadCacheExpiredLockFetchFails
testLoadCacheExpiredLockFetchFails()
EtcdConfig::load.
Definition: EtcdConfigTest.php:312
EtcConfigTest\createSimpleConfigMock
createSimpleConfigMock(array $config)
Definition: EtcdConfigTest.php:18
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
EtcConfigTest\testConstructCacheSpec
testConstructCacheSpec()
EtcdConfig::__construct.
Definition: EtcdConfigTest.php:91
EtcConfigTest\testLoadCacheMissBackendError
testLoadCacheMissBackendError()
EtcdConfig::load.
Definition: EtcdConfigTest.php:175
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
EtcConfigTest\testHasKnown
testHasKnown()
EtcdConfig::has.
Definition: EtcdConfigTest.php:32
EtcConfigTest\testConstructCacheObj
testConstructCacheObj()
EtcdConfig::__construct.
Definition: EtcdConfigTest.php:74
EtcConfigTest\testLoadCacheMiss
testLoadCacheMiss()
Test matrix.
Definition: EtcdConfigTest.php:150
EtcConfigTest\testLoadCacheExpiredNoLock
testLoadCacheExpiredNoLock()
EtcdConfig::load.
Definition: EtcdConfigTest.php:341
EtcConfigTest\createConfigMock
createConfigMock(array $options=[])
Definition: EtcdConfigTest.php:7
EtcConfigTest\testLoadCacheMissWithoutLock
testLoadCacheMissWithoutLock()
EtcdConfig::load.
Definition: EtcdConfigTest.php:201
EtcConfigTest\testLoadCacheHit
testLoadCacheHit()
EtcdConfig::load.
Definition: EtcdConfigTest.php:232
$cache
$cache
Definition: mcc.php:33
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1965
EtcConfigTest\testGetKnown
testGetKnown()
EtcdConfig::__construct EtcdConfig::get.
Definition: EtcdConfigTest.php:43
true
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1965
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
EtcConfigTest\testHasUnknown
testHasUnknown()
EtcdConfig::has.
Definition: EtcdConfigTest.php:53
EtcConfigTest\testGetUnknown
testGetUnknown()
EtcdConfig::get.
Definition: EtcdConfigTest.php:63
array
the array() calling protocol came about after MediaWiki 1.4rc1.
EtcConfigTest\testLoadProcessCacheHit
testLoadProcessCacheHit()
EtcdConfig::load.
Definition: EtcdConfigTest.php:257