Merge "New helper ApiTestCase::setExpectedApiException()"
[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 $insertCallback = null
88 ) {
89 return new NameTableStore(
90 $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ),
91 $this->getHashWANObjectCache( $cacheBag ),
92 new NullLogger(),
93 'slot_roles', 'role_id', 'role_name',
94 $normalizationCallback,
95 false,
96 $insertCallback
97 );
98 }
99
100 public function provideGetAndAcquireId() {
101 return [
102 'no wancache, empty table' =>
103 [ new EmptyBagOStuff(), true, 1, [], 'foo', 1 ],
104 'no wancache, one matching value' =>
105 [ new EmptyBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ],
106 'no wancache, one not matching value' =>
107 [ new EmptyBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ],
108 'no wancache, multiple, one matching value' =>
109 [ new EmptyBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ],
110 'no wancache, multiple, no matching value' =>
111 [ new EmptyBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ],
112 'wancache, empty table' =>
113 [ new HashBagOStuff(), true, 1, [], 'foo', 1 ],
114 'wancache, one matching value' =>
115 [ new HashBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ],
116 'wancache, one not matching value' =>
117 [ new HashBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ],
118 'wancache, multiple, one matching value' =>
119 [ new HashBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ],
120 'wancache, multiple, no matching value' =>
121 [ new HashBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ],
122 ];
123 }
124
125 /**
126 * @dataProvider provideGetAndAcquireId
127 * @param BagOStuff $cacheBag to use in the WANObjectCache service
128 * @param bool $needsInsert Does the value we are testing need to be inserted?
129 * @param int $selectCalls Number of times the select DB method will be called
130 * @param string[] $existingValues to be added to the db table
131 * @param string $name name to acquire
132 * @param int $expectedId the id we expect the name to have
133 */
134 public function testGetAndAcquireId(
135 $cacheBag,
136 $needsInsert,
137 $selectCalls,
138 $existingValues,
139 $name,
140 $expectedId
141 ) {
142 $this->populateTable( $existingValues );
143 $store = $this->getNameTableSqlStore( $cacheBag, (int)$needsInsert, $selectCalls );
144
145 // Some names will not initially exist
146 try {
147 $result = $store->getId( $name );
148 $this->assertSame( $expectedId, $result );
149 } catch ( NameTableAccessException $e ) {
150 if ( $needsInsert ) {
151 $this->assertTrue( true ); // Expected exception
152 } else {
153 $this->fail( 'Did not expect an exception, but got one: ' . $e->getMessage() );
154 }
155 }
156
157 // All names should return their id here
158 $this->assertSame( $expectedId, $store->acquireId( $name ) );
159
160 // acquireId inserted these names, so now everything should exist with getId
161 $this->assertSame( $expectedId, $store->getId( $name ) );
162
163 // calling getId again will also still work, and not result in more selects
164 $this->assertSame( $expectedId, $store->getId( $name ) );
165 }
166
167 public function provideTestGetAndAcquireIdNameNormalization() {
168 yield [ 'A', 'a', 'strtolower' ];
169 yield [ 'b', 'B', 'strtoupper' ];
170 yield [
171 'X',
172 'X',
173 function ( $name ) {
174 return $name;
175 }
176 ];
177 yield [ 'ZZ', 'ZZ-a', __CLASS__ . '::appendDashAToString' ];
178 }
179
180 public static function appendDashAToString( $string ) {
181 return $string . '-a';
182 }
183
184 /**
185 * @dataProvider provideTestGetAndAcquireIdNameNormalization
186 */
187 public function testGetAndAcquireIdNameNormalization(
188 $nameIn,
189 $nameOut,
190 $normalizationCallback
191 ) {
192 $store = $this->getNameTableSqlStore(
193 new EmptyBagOStuff(),
194 1,
195 1,
196 $normalizationCallback
197 );
198 $acquiredId = $store->acquireId( $nameIn );
199 $this->assertSame( $nameOut, $store->getName( $acquiredId ) );
200 }
201
202 public function provideGetName() {
203 return [
204 [ new HashBagOStuff(), 3, 3 ],
205 [ new EmptyBagOStuff(), 3, 3 ],
206 ];
207 }
208
209 /**
210 * @dataProvider provideGetName
211 */
212 public function testGetName( $cacheBag, $insertCalls, $selectCalls ) {
213 $store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls );
214
215 // Get 1 ID and make sure getName returns correctly
216 $fooId = $store->acquireId( 'foo' );
217 $this->assertSame( 'foo', $store->getName( $fooId ) );
218
219 // Get another ID and make sure getName returns correctly
220 $barId = $store->acquireId( 'bar' );
221 $this->assertSame( 'bar', $store->getName( $barId ) );
222
223 // Blitz the cache and make sure it still returns
224 TestingAccessWrapper::newFromObject( $store )->tableCache = null;
225 $this->assertSame( 'foo', $store->getName( $fooId ) );
226 $this->assertSame( 'bar', $store->getName( $barId ) );
227
228 // Blitz the cache again and get another ID and make sure getName returns correctly
229 TestingAccessWrapper::newFromObject( $store )->tableCache = null;
230 $bazId = $store->acquireId( 'baz' );
231 $this->assertSame( 'baz', $store->getName( $bazId ) );
232 $this->assertSame( 'baz', $store->getName( $bazId ) );
233 }
234
235 public function testGetName_masterFallback() {
236 $store = $this->getNameTableSqlStore( new EmptyBagOStuff(), 1, 2 );
237
238 // Insert a new name
239 $fooId = $store->acquireId( 'foo' );
240
241 // Empty the process cache, getCachedTable() will now return this empty array
242 TestingAccessWrapper::newFromObject( $store )->tableCache = [];
243
244 // getName should fallback to master, which is why we assert 2 selectCalls above
245 $this->assertSame( 'foo', $store->getName( $fooId ) );
246 }
247
248 public function testGetMap_empty() {
249 $this->populateTable( [] );
250 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
251 $table = $store->getMap();
252 $this->assertSame( [], $table );
253 }
254
255 public function testGetMap_twoValues() {
256 $this->populateTable( [ 'foo', 'bar' ] );
257 $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 );
258
259 // We are using a cache, so 2 calls should only result in 1 select on the db
260 $store->getMap();
261 $table = $store->getMap();
262
263 $expected = [ 1 => 'foo', 2 => 'bar' ];
264 $this->assertSame( $expected, $table );
265 // Make sure the table returned is the same as the cached table
266 $this->assertSame( $expected, TestingAccessWrapper::newFromObject( $store )->tableCache );
267 }
268
269 public function testCacheRaceCondition() {
270 $wanHashBag = new HashBagOStuff();
271 $store1 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 );
272 $store2 = $this->getNameTableSqlStore( $wanHashBag, 1, 0 );
273 $store3 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 );
274
275 // Cache the current table in the instances we will use
276 // This simulates multiple requests running simultaneously
277 $store1->getMap();
278 $store2->getMap();
279 $store3->getMap();
280
281 // Store 2 separate names using different instances
282 $fooId = $store1->acquireId( 'foo' );
283 $barId = $store2->acquireId( 'bar' );
284
285 // Each of these instances should be aware of what they have inserted
286 $this->assertSame( $fooId, $store1->acquireId( 'foo' ) );
287 $this->assertSame( $barId, $store2->acquireId( 'bar' ) );
288
289 // A new store should be able to get both of these new Ids
290 // Note: before there was a race condition here where acquireId( 'bar' ) would update the
291 // cache with data missing the 'foo' key that it was not aware of
292 $store4 = $this->getNameTableSqlStore( $wanHashBag, 0, 1 );
293 $this->assertSame( $fooId, $store4->getId( 'foo' ) );
294 $this->assertSame( $barId, $store4->getId( 'bar' ) );
295
296 // If a store with old cached data tries to acquire these we will get the same ids.
297 $this->assertSame( $fooId, $store3->acquireId( 'foo' ) );
298 $this->assertSame( $barId, $store3->acquireId( 'bar' ) );
299 }
300
301 public function testGetAndAcquireIdInsertCallback() {
302 // FIXME: fails under postgres
303 $this->markTestSkippedIfDbType( 'postgres' );
304
305 $store = $this->getNameTableSqlStore(
306 new EmptyBagOStuff(),
307 1,
308 1,
309 null,
310 function ( $insertFields ) {
311 $insertFields['role_id'] = 7251;
312 return $insertFields;
313 }
314 );
315 $this->assertSame( 7251, $store->acquireId( 'A' ) );
316 }
317
318 }