MediaWiki REL1_30
EtcdConfigTest.php
Go to the documentation of this file.
1<?php
2
3use Wikimedia\TestingAccessWrapper;
4
5class 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
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
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}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
createConfigMock(array $options=[])
testLoadCacheMiss()
Test matrix.
testLoadProcessCacheHit()
EtcdConfig::load.
testLoadCacheMissWithoutLock()
EtcdConfig::load.
testConstructCacheObj()
EtcdConfig::__construct.
testLoadCacheExpiredNoLock()
EtcdConfig::load.
createSimpleConfigMock(array $config)
testLoadCacheExpiredLockFetchSucceeded()
EtcdConfig::load.
testConstructCacheSpec()
EtcdConfig::__construct.
testGetUnknown()
EtcdConfig::get.
testLoadCacheExpiredLockFetchFails()
EtcdConfig::load.
testFetchFromServer(array $httpResponse, array $expected)
EtcdConfig::fetchAllFromEtcdServer EtcdConfig::unserialize EtcdConfig::parseResponse EtcdConfig::pars...
testHasUnknown()
EtcdConfig::has.
testGetKnown()
EtcdConfig::__construct EtcdConfig::get.
testHasKnown()
EtcdConfig::has.
static provideFetchFromServer()
testLoadCacheHit()
EtcdConfig::load.
testLoadCacheMissBackendError()
EtcdConfig::load.
the array() calling protocol came about after MediaWiki 1.4rc1.
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:1971
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:1976
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
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:37
$cache
Definition mcc.php:33