3 use Wikimedia\ScopedCallback
;
6 * @author Matthias Mullie <mmullie@wikimedia.org>
9 class BagOStuffTest
extends MediaWikiTestCase
{
13 const TEST_KEY
= 'test';
15 protected function setUp() {
18 // type defined through parameter
19 if ( $this->getCliArg( 'use-bagostuff' ) ) {
20 $name = $this->getCliArg( 'use-bagostuff' );
22 $this->cache
= ObjectCache
::newFromId( $name );
24 // no type defined - use simple hash
25 $this->cache
= new HashBagOStuff
;
28 $this->cache
->delete( $this->cache
->makeKey( self
::TEST_KEY
) );
32 * @covers BagOStuff::makeGlobalKey
33 * @covers BagOStuff::makeKeyInternal
35 public function testMakeKey() {
36 $cache = ObjectCache
::newFromId( 'hash' );
38 $localKey = $cache->makeKey( 'first', 'second', 'third' );
39 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
41 $this->assertStringMatchesFormat(
42 '%Sfirst%Ssecond%Sthird%S',
44 'Local key interpolates parameters'
47 $this->assertStringMatchesFormat(
48 'global%Sfirst%Ssecond%Sthird%S',
50 'Global key interpolates parameters and contains global prefix'
53 $this->assertNotEquals(
56 'Local key and global key with same parameters should not be equal'
59 $this->assertNotEquals(
60 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc:', 'de' ] ),
61 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc', ':de' ] )
66 * @covers BagOStuff::merge
67 * @covers BagOStuff::mergeViaLock
68 * @covers BagOStuff::mergeViaCas
70 public function testMerge() {
72 $key = $this->cache
->makeKey( self
::TEST_KEY
);
73 $callback = function ( BagOStuff
$cache, $key, $oldVal ) use ( &$calls ) {
76 return ( $oldVal === false ) ?
'merged' : $oldVal . 'merged';
79 // merge on non-existing value
80 $merged = $this->cache
->merge( $key, $callback, 5 );
81 $this->assertTrue( $merged );
82 $this->assertEquals( 'merged', $this->cache
->get( $key ) );
84 // merge on existing value
85 $merged = $this->cache
->merge( $key, $callback, 5 );
86 $this->assertTrue( $merged );
87 $this->assertEquals( 'mergedmerged', $this->cache
->get( $key ) );
90 $this->cache
->lock( $key );
91 $this->assertFalse( $this->cache
->merge( $key, $callback, 1 ), 'Non-blocking merge' );
92 $this->cache
->unlock( $key );
93 $this->assertEquals( 0, $calls );
97 * @covers BagOStuff::merge
98 * @covers BagOStuff::mergeViaLock
100 public function testMerge_fork() {
101 $key = $this->cache
->makeKey( self
::TEST_KEY
);
102 $callback = function ( BagOStuff
$cache, $key, $oldVal ) {
103 return ( $oldVal === false ) ?
'merged' : $oldVal . 'merged';
106 * Test concurrent merges by forking this process, if:
107 * - not manually called with --use-bagostuff
108 * - pcntl_fork is supported by the system
109 * - cache type will correctly support calls over forks
111 $fork = (bool)$this->getCliArg( 'use-bagostuff' );
112 $fork &= function_exists( 'pcntl_fork' );
113 $fork &= !$this->cache
instanceof HashBagOStuff
;
114 $fork &= !$this->cache
instanceof EmptyBagOStuff
;
115 $fork &= !$this->cache
instanceof MultiWriteBagOStuff
;
118 // Function to start merge(), run another merge() midway through, then finish
119 $outerFunc = function ( BagOStuff
$cache, $key, $oldVal ) use ( $callback, &$pid ) {
124 pcntl_wait( $status );
126 return $callback( $cache, $key, $oldVal );
128 $this->cache
->merge( $key, $callback, 0, 1 );
129 // Bail out of the outer merge() in the child process since it does not
130 // need to attempt to write anything. Success is checked by the parent.
131 parent
::tearDown(); // avoid phpunit notices
136 // attempt a merge - this should fail
137 $merged = $this->cache
->merge( $key, $outerFunc, 0, 1 );
140 return; // can't fork, ignore this test...
143 // merge has failed because child process was merging (and we only attempted once)
144 $this->assertFalse( $merged );
146 // make sure the child's merge is completed and verify
147 $this->assertEquals( $this->cache
->get( $key ), 'mergedmerged' );
149 $this->markTestSkipped( 'No pcntl methods available' );
154 * @covers BagOStuff::changeTTL
156 public function testChangeTTL() {
157 $key = $this->cache
->makeKey( self
::TEST_KEY
);
160 $this->cache
->add( $key, $value, 5 );
161 $this->assertTrue( $this->cache
->changeTTL( $key, 5 ) );
162 $this->assertEquals( $this->cache
->get( $key ), $value );
163 $this->cache
->delete( $key );
164 $this->assertFalse( $this->cache
->changeTTL( $key, 5 ) );
168 * @covers BagOStuff::add
170 public function testAdd() {
171 $key = $this->cache
->makeKey( self
::TEST_KEY
);
172 $this->assertTrue( $this->cache
->add( $key, 'test', 5 ) );
176 * @covers BagOStuff::get
178 public function testGet() {
179 $value = [ 'this' => 'is', 'a' => 'test' ];
181 $key = $this->cache
->makeKey( self
::TEST_KEY
);
182 $this->cache
->add( $key, $value, 5 );
183 $this->assertEquals( $this->cache
->get( $key ), $value );
187 * @covers BagOStuff::get
188 * @covers BagOStuff::set
189 * @covers BagOStuff::getWithSetCallback
191 public function testGetWithSetCallback() {
192 $key = $this->cache
->makeKey( self
::TEST_KEY
);
193 $value = $this->cache
->getWithSetCallback(
197 return 'hello kitty';
201 $this->assertEquals( 'hello kitty', $value );
202 $this->assertEquals( $value, $this->cache
->get( $key ) );
206 * @covers BagOStuff::incr
208 public function testIncr() {
209 $key = $this->cache
->makeKey( self
::TEST_KEY
);
210 $this->cache
->add( $key, 0, 5 );
211 $this->cache
->incr( $key );
213 $actualValue = $this->cache
->get( $key );
214 $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
218 * @covers BagOStuff::incrWithInit
220 public function testIncrWithInit() {
221 $key = $this->cache
->makeKey( self
::TEST_KEY
);
222 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
223 $this->assertEquals( 3, $val, "Correct init value" );
225 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
226 $this->assertEquals( 4, $val, "Correct init value" );
230 * @covers BagOStuff::getMulti
232 public function testGetMulti() {
233 $value1 = [ 'this' => 'is', 'a' => 'test' ];
234 $value2 = [ 'this' => 'is', 'another' => 'test' ];
235 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
236 $value4 = [ 'another test where chars in key will be encoded' ];
238 $key1 = $this->cache
->makeKey( 'test-1' );
239 $key2 = $this->cache
->makeKey( 'test-2' );
240 // internally, MemcachedBagOStuffs will encode to will-%25-encode
241 $key3 = $this->cache
->makeKey( 'will-%-encode' );
242 $key4 = $this->cache
->makeKey(
243 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
247 $this->cache
->delete( $key1 );
248 $this->cache
->delete( $key2 );
249 $this->cache
->delete( $key3 );
250 $this->cache
->delete( $key4 );
252 $this->cache
->add( $key1, $value1, 5 );
253 $this->cache
->add( $key2, $value2, 5 );
254 $this->cache
->add( $key3, $value3, 5 );
255 $this->cache
->add( $key4, $value4, 5 );
258 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
259 $this->cache
->getMulti( [ $key1, $key2, $key3, $key4 ] )
263 $this->cache
->delete( $key1 );
264 $this->cache
->delete( $key2 );
265 $this->cache
->delete( $key3 );
266 $this->cache
->delete( $key4 );
270 * @covers BagOStuff::getScopedLock
272 public function testGetScopedLock() {
273 $key = $this->cache
->makeKey( self
::TEST_KEY
);
274 $value1 = $this->cache
->getScopedLock( $key, 0 );
275 $value2 = $this->cache
->getScopedLock( $key, 0 );
277 $this->assertType( ScopedCallback
::class, $value1, 'First call returned lock' );
278 $this->assertNull( $value2, 'Duplicate call returned no lock' );
282 $value3 = $this->cache
->getScopedLock( $key, 0 );
283 $this->assertType( ScopedCallback
::class, $value3, 'Lock returned callback after release' );
286 $value1 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
287 $value2 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
289 $this->assertType( ScopedCallback
::class, $value1, 'First reentrant call returned lock' );
290 $this->assertType( ScopedCallback
::class, $value1, 'Second reentrant call returned lock' );
294 * @covers BagOStuff::__construct
295 * @covers BagOStuff::trackDuplicateKeys
297 public function testReportDupes() {
298 $logger = $this->createMock( Psr\Log\NullLogger
::class );
299 $logger->expects( $this->once() )
300 ->method( 'warning' )
301 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
306 $cache = new HashBagOStuff( [
307 'reportDupes' => true,
308 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
311 $cache->get( 'foo' );
312 $cache->get( 'bar' );
313 $cache->get( 'foo' );
315 DeferredUpdates
::doUpdates();
319 * @covers BagOStuff::lock()
320 * @covers BagOStuff::unlock()
322 public function testLocking() {
324 $this->assertTrue( $this->cache
->lock( $key ) );
325 $this->assertFalse( $this->cache
->lock( $key ) );
326 $this->assertTrue( $this->cache
->unlock( $key ) );
329 $this->assertTrue( $this->cache
->lock( $key2, 5, 5, 'rclass' ) );
330 $this->assertTrue( $this->cache
->lock( $key2, 5, 5, 'rclass' ) );
331 $this->assertTrue( $this->cache
->unlock( $key2 ) );
332 $this->assertTrue( $this->cache
->unlock( $key2 ) );