Merge "Add support for 'hu-formal'"
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / NameTableStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use BagOStuff;
6 use EmptyBagOStuff;
7 use HashBagOStuff;
8 use MediaWiki\Storage\NameTableAccessException;
9 use MediaWiki\Storage\NameTableStore;
10 use MediaWikiTestCase;
11 use Psr\Log\NullLogger;
12 use WANObjectCache;
13 use Wikimedia\Rdbms\Database;
14 use Wikimedia\Rdbms\LoadBalancer;
15 use Wikimedia\TestingAccessWrapper;
16
17 /**
18 * @author Addshore
19 * @group Database
20 * @covers \MediaWiki\Storage\NameTableStore
21 */
22 class NameTableStoreTest extends MediaWikiTestCase {
23
24 public function setUp() {
25 $this->tablesUsed[] = 'slot_roles';
26 parent::setUp();
27 }
28
29 private function populateTable( $values ) {
30 $insertValues = [];
31 foreach ( $values as $name ) {
32 $insertValues[] = [ 'role_name' => $name ];
33 }
34 $this->db->insert( 'slot_roles', $insertValues );
35 }
36
37 private function getHashWANObjectCache( $cacheBag ) {
38 return new WANObjectCache( [ 'cache' => $cacheBag ] );
39 }
40
41 /**
42 * @param $db
43 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
44 */
45 private function getMockLoadBalancer( $db ) {
46 $mock = $this->getMockBuilder( LoadBalancer::class )
47 ->disableOriginalConstructor()
48 ->getMock();
49 $mock->expects( $this->any() )
50 ->method( 'getConnection' )
51 ->willReturn( $db );
52 return $mock;
53 }
54
55 private function getCallCheckingDb( $insertCalls, $selectCalls ) {
56 $mock = $this->getMockBuilder( Database::class )
57 ->disableOriginalConstructor()
58 ->getMock();
59 $mock->expects( $this->exactly( $insertCalls ) )
60 ->method( 'insert' )
61 ->willReturnCallback( function () {
62 return call_user_func_array( [ $this->db, 'insert' ], func_get_args() );
63 } );
64 $mock->expects( $this->exactly( $selectCalls ) )
65 ->method( 'select' )
66 ->willReturnCallback( function () {
67 return call_user_func_array( [ $this->db, 'select' ], func_get_args() );
68 } );
69 $mock->expects( $this->exactly( $insertCalls ) )
70 ->method( 'affectedRows' )
71 ->willReturnCallback( function () {
72 return call_user_func_array( [ $this->db, 'affectedRows' ], func_get_args() );
73 } );
74 $mock->expects( $this->any() )
75 ->method( 'insertId' )
76 ->willReturnCallback( function () {
77 return call_user_func_array( [ $this->db, 'insertId' ], func_get_args() );
78 } );
79 return $mock;
80 }
81
82 private function getNameTableSqlStore(
83 BagOStuff $cacheBag,
84 $insertCalls,
85 $selectCalls,
86 $normalizationCallback = null
87 ) {
88 return new NameTableStore(
89 $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ),
90 $this->getHashWANObjectCache( $cacheBag ),
91 new NullLogger(),
92 'slot_roles', 'role_id', 'role_name',
93 $normalizationCallback
94 );
95 }
96
97 public function provideGetAndAcquireId() {
98 return [
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 ],
119 ];
120 }
121
122 /**
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
130 */
131 public function testGetAndAcquireId(
132 $cacheBag,
133 $needsInsert,
134 $selectCalls,
135 $existingValues,
136 $name,
137 $expectedId
138 ) {
139 $this->populateTable( $existingValues );
140 $store = $this->getNameTableSqlStore( $cacheBag, (int)$needsInsert, $selectCalls );
141
142 // Some names will not initially exist
143 try {
144 $result = $store->getId( $name );
145 $this->assertSame( $expectedId, $result );
146 } catch ( NameTableAccessException $e ) {
147 if ( $needsInsert ) {
148 $this->assertTrue( true ); // Expected exception
149 } else {
150 $this->fail( 'Did not expect an exception, but got one: ' . $e->getMessage() );
151 }
152 }
153
154 // All names should return their id here
155 $this->assertSame( $expectedId, $store->acquireId( $name ) );
156
157 // acquireId inserted these names, so now everything should exist with getId
158 $this->assertSame( $expectedId, $store->getId( $name ) );
159
160 // calling getId again will also still work, and not result in more selects
161 $this->assertSame( $expectedId, $store->getId( $name ) );
162 }
163
164 public function provideTestGetAndAcquireIdNameNormalization() {
165 yield [ 'A', 'a', 'strtolower' ];
166 yield [ 'b', 'B', 'strtoupper' ];
167 yield [
168 'X',
169 'X',
170 function ( $name ) {
171 return $name;
172 }
173 ];
174 yield [ 'ZZ', 'ZZ-a', __CLASS__ . '::appendDashAToString' ];
175 }
176
177 public static function appendDashAToString( $string ) {
178 return $string . '-a';
179 }
180
181 /**
182 * @dataProvider provideTestGetAndAcquireIdNameNormalization
183 */
184 public function testGetAndAcquireIdNameNormalization(
185 $nameIn,
186 $nameOut,
187 $normalizationCallback
188 ) {
189 $store = $this->getNameTableSqlStore(
190 new EmptyBagOStuff(),
191 1,
192 1,
193 $normalizationCallback
194 );
195 $acquiredId = $store->acquireId( $nameIn );
196 $this->assertSame( $nameOut, $store->getName( $acquiredId ) );
197 }
198
199 public function provideGetName() {
200 return [
201 [ new HashBagOStuff(), 3, 3 ],
202 [ new EmptyBagOStuff(), 3, 3 ],
203 ];
204 }
205
206 /**
207 * @dataProvider provideGetName
208 */
209 public function testGetName( $cacheBag, $insertCalls, $selectCalls ) {
210 $store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls );
211
212 // Get 1 ID and make sure getName returns correctly
213 $fooId = $store->acquireId( 'foo' );
214 $this->assertSame( 'foo', $store->getName( $fooId ) );
215
216 // Get another ID and make sure getName returns correctly
217 $barId = $store->acquireId( 'bar' );
218 $this->assertSame( 'bar', $store->getName( $barId ) );
219
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 ) );
224
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 ) );
230 }
231
232 public function testGetName_masterFallback() {
233 $store = $this->getNameTableSqlStore( new EmptyBagOStuff(), 1, 2 );
234
235 // Insert a new name
236 $fooId = $store->acquireId( 'foo' );
237
238 // Empty the process cache, getCachedTable() will now return this empty array
239 TestingAccessWrapper::newFromObject( $store )->tableCache = [];
240
241 // getName should fallback to master, which is why we assert 2 selectCalls above
242 $this->assertSame( 'foo', $store->getName( $fooId ) );
243 }
244
245 public function testGetMap_empty() {
246 $this->populateTable( [] );
247 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
248 $table = $store->getMap();
249 $this->assertSame( [], $table );
250 }
251
252 public function testGetMap_twoValues() {
253 $this->populateTable( [ 'foo', 'bar' ] );
254 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
255
256 // We are using a cache, so 2 calls should only result in 1 select on the db
257 $store->getMap();
258 $table = $store->getMap();
259
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 );
264 }
265
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 );
271
272 // Cache the current table in the instances we will use
273 // This simulates multiple requests running simultaneously
274 $store1->getMap();
275 $store2->getMap();
276 $store3->getMap();
277
278 // Store 2 separate names using different instances
279 $fooId = $store1->acquireId( 'foo' );
280 $barId = $store2->acquireId( 'bar' );
281
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' ) );
285
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' ) );
292
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' ) );
296 }
297
298 }