3 namespace MediaWiki\Tests\Storage
;
8 use MediaWiki\Storage\NameTableAccessException
;
9 use MediaWiki\Storage\NameTableStore
;
10 use MediaWikiTestCase
;
11 use Psr\Log\NullLogger
;
13 use Wikimedia\Rdbms\Database
;
14 use Wikimedia\Rdbms\LoadBalancer
;
15 use Wikimedia\TestingAccessWrapper
;
20 * @covers \MediaWiki\Storage\NameTableStore
22 class NameTableStoreTest
extends MediaWikiTestCase
{
24 public function setUp() {
25 $this->tablesUsed
[] = 'slot_roles';
29 private function populateTable( $values ) {
31 foreach ( $values as $name ) {
32 $insertValues[] = [ 'role_name' => $name ];
34 $this->db
->insert( 'slot_roles', $insertValues );
37 private function getHashWANObjectCache( $cacheBag ) {
38 return new WANObjectCache( [ 'cache' => $cacheBag ] );
43 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
45 private function getMockLoadBalancer( $db ) {
46 $mock = $this->getMockBuilder( LoadBalancer
::class )
47 ->disableOriginalConstructor()
49 $mock->expects( $this->any() )
50 ->method( 'getConnection' )
55 private function getCallCheckingDb( $insertCalls, $selectCalls ) {
56 $mock = $this->getMockBuilder( Database
::class )
57 ->disableOriginalConstructor()
59 $mock->expects( $this->exactly( $insertCalls ) )
61 ->willReturnCallback( function () {
62 return call_user_func_array( [ $this->db
, 'insert' ], func_get_args() );
64 $mock->expects( $this->exactly( $selectCalls ) )
66 ->willReturnCallback( function () {
67 return call_user_func_array( [ $this->db
, 'select' ], func_get_args() );
69 $mock->expects( $this->exactly( $insertCalls ) )
70 ->method( 'affectedRows' )
71 ->willReturnCallback( function () {
72 return call_user_func_array( [ $this->db
, 'affectedRows' ], func_get_args() );
74 $mock->expects( $this->any() )
75 ->method( 'insertId' )
76 ->willReturnCallback( function () {
77 return call_user_func_array( [ $this->db
, 'insertId' ], func_get_args() );
82 private function getNameTableSqlStore(
86 $normalizationCallback = null
88 return new NameTableStore(
89 $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ),
90 $this->getHashWANObjectCache( $cacheBag ),
92 'slot_roles', 'role_id', 'role_name',
93 $normalizationCallback
97 public function provideGetAndAcquireId() {
99 'no wancache, empty table' =>
100 [ new EmptyBagOStuff(), true, 1, [], 'foo', 1 ],
101 'no wancache, one matching value' =>
102 [ new EmptyBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ],
103 'no wancache, one not matching value' =>
104 [ new EmptyBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ],
105 'no wancache, multiple, one matching value' =>
106 [ new EmptyBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ],
107 'no wancache, multiple, no matching value' =>
108 [ new EmptyBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ],
109 'wancache, empty table' =>
110 [ new HashBagOStuff(), true, 1, [], 'foo', 1 ],
111 'wancache, one matching value' =>
112 [ new HashBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ],
113 'wancache, one not matching value' =>
114 [ new HashBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ],
115 'wancache, multiple, one matching value' =>
116 [ new HashBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ],
117 'wancache, multiple, no matching value' =>
118 [ new HashBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ],
123 * @dataProvider provideGetAndAcquireId
124 * @param BagOStuff $cacheBag to use in the WANObjectCache service
125 * @param bool $needsInsert Does the value we are testing need to be inserted?
126 * @param int $selectCalls Number of times the select DB method will be called
127 * @param string[] $existingValues to be added to the db table
128 * @param string $name name to acquire
129 * @param int $expectedId the id we expect the name to have
131 public function testGetAndAcquireId(
139 $this->populateTable( $existingValues );
140 $store = $this->getNameTableSqlStore( $cacheBag, (int)$needsInsert, $selectCalls );
142 // Some names will not initially exist
144 $result = $store->getId( $name );
145 $this->assertSame( $expectedId, $result );
146 } catch ( NameTableAccessException
$e ) {
147 if ( $needsInsert ) {
148 $this->assertTrue( true ); // Expected exception
150 $this->fail( 'Did not expect an exception, but got one: ' . $e->getMessage() );
154 // All names should return their id here
155 $this->assertSame( $expectedId, $store->acquireId( $name ) );
157 // acquireId inserted these names, so now everything should exist with getId
158 $this->assertSame( $expectedId, $store->getId( $name ) );
160 // calling getId again will also still work, and not result in more selects
161 $this->assertSame( $expectedId, $store->getId( $name ) );
164 public function provideTestGetAndAcquireIdNameNormalization() {
165 yield
[ 'A', 'a', 'strtolower' ];
166 yield
[ 'b', 'B', 'strtoupper' ];
174 yield
[ 'ZZ', 'ZZ-a', __CLASS__
. '::appendDashAToString' ];
177 public static function appendDashAToString( $string ) {
178 return $string . '-a';
182 * @dataProvider provideTestGetAndAcquireIdNameNormalization
184 public function testGetAndAcquireIdNameNormalization(
187 $normalizationCallback
189 $store = $this->getNameTableSqlStore(
190 new EmptyBagOStuff(),
193 $normalizationCallback
195 $acquiredId = $store->acquireId( $nameIn );
196 $this->assertSame( $nameOut, $store->getName( $acquiredId ) );
199 public function provideGetName() {
201 [ new HashBagOStuff(), 3, 3 ],
202 [ new EmptyBagOStuff(), 3, 3 ],
207 * @dataProvider provideGetName
209 public function testGetName( $cacheBag, $insertCalls, $selectCalls ) {
210 $store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls );
212 // Get 1 ID and make sure getName returns correctly
213 $fooId = $store->acquireId( 'foo' );
214 $this->assertSame( 'foo', $store->getName( $fooId ) );
216 // Get another ID and make sure getName returns correctly
217 $barId = $store->acquireId( 'bar' );
218 $this->assertSame( 'bar', $store->getName( $barId ) );
220 // Blitz the cache and make sure it still returns
221 TestingAccessWrapper
::newFromObject( $store )->tableCache
= null;
222 $this->assertSame( 'foo', $store->getName( $fooId ) );
223 $this->assertSame( 'bar', $store->getName( $barId ) );
225 // Blitz the cache again and get another ID and make sure getName returns correctly
226 TestingAccessWrapper
::newFromObject( $store )->tableCache
= null;
227 $bazId = $store->acquireId( 'baz' );
228 $this->assertSame( 'baz', $store->getName( $bazId ) );
229 $this->assertSame( 'baz', $store->getName( $bazId ) );
232 public function testGetName_masterFallback() {
233 $store = $this->getNameTableSqlStore( new EmptyBagOStuff(), 1, 2 );
236 $fooId = $store->acquireId( 'foo' );
238 // Empty the process cache, getCachedTable() will now return this empty array
239 TestingAccessWrapper
::newFromObject( $store )->tableCache
= [];
241 // getName should fallback to master, which is why we assert 2 selectCalls above
242 $this->assertSame( 'foo', $store->getName( $fooId ) );
245 public function testGetMap_empty() {
246 $this->populateTable( [] );
247 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
248 $table = $store->getMap();
249 $this->assertSame( [], $table );
252 public function testGetMap_twoValues() {
253 $this->populateTable( [ 'foo', 'bar' ] );
254 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
256 // We are using a cache, so 2 calls should only result in 1 select on the db
258 $table = $store->getMap();
260 $expected = [ 2 => 'bar', 1 => 'foo' ];
261 $this->assertSame( $expected, $table );
262 // Make sure the table returned is the same as the cached table
263 $this->assertSame( $expected, TestingAccessWrapper
::newFromObject( $store )->tableCache
);
266 public function testCacheRaceCondition() {
267 $wanHashBag = new HashBagOStuff();
268 $store1 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 );
269 $store2 = $this->getNameTableSqlStore( $wanHashBag, 1, 0 );
270 $store3 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 );
272 // Cache the current table in the instances we will use
273 // This simulates multiple requests running simultaneously
278 // Store 2 separate names using different instances
279 $fooId = $store1->acquireId( 'foo' );
280 $barId = $store2->acquireId( 'bar' );
282 // Each of these instances should be aware of what they have inserted
283 $this->assertSame( $fooId, $store1->acquireId( 'foo' ) );
284 $this->assertSame( $barId, $store2->acquireId( 'bar' ) );
286 // A new store should be able to get both of these new Ids
287 // Note: before there was a race condition here where acquireId( 'bar' ) would update the
288 // cache with data missing the 'foo' key that it was not aware of
289 $store4 = $this->getNameTableSqlStore( $wanHashBag, 0, 1 );
290 $this->assertSame( $fooId, $store4->getId( 'foo' ) );
291 $this->assertSame( $barId, $store4->getId( 'bar' ) );
293 // If a store with old cached data tries to acquire these we will get the same ids.
294 $this->assertSame( $fooId, $store3->acquireId( 'foo' ) );
295 $this->assertSame( $barId, $store3->acquireId( 'bar' ) );