Merge "Test ApiUserrights"
[lhc/web/wiklou.git] / tests / phpunit / includes / utils / BatchRowUpdateTest.php
1 <?php
2
3 /**
4 * Tests for BatchRowUpdate and its components
5 *
6 * @group db
7 *
8 * @covers BatchRowUpdate
9 * @covers BatchRowIterator
10 * @covers BatchRowWriter
11 */
12 class BatchRowUpdateTest extends MediaWikiTestCase {
13
14 public function testWriterBasicFunctionality() {
15 $db = $this->mockDb();
16 $writer = new BatchRowWriter( $db, 'echo_event' );
17
18 $updates = [
19 self::mockUpdate( [ 'something' => 'changed' ] ),
20 self::mockUpdate( [ 'otherthing' => 'changed' ] ),
21 self::mockUpdate( [ 'and' => 'something', 'else' => 'changed' ] ),
22 ];
23
24 $db->expects( $this->exactly( count( $updates ) ) )
25 ->method( 'update' );
26
27 $writer->write( $updates );
28 }
29
30 protected static function mockUpdate( array $changes ) {
31 static $i = 0;
32 return [
33 'primaryKey' => [ 'event_id' => $i++ ],
34 'changes' => $changes,
35 ];
36 }
37
38 public function testReaderBasicIterate() {
39 $db = $this->mockDb();
40 $batchSize = 2;
41 $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize );
42
43 $response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () {
44 static $i = 0;
45 return [ 'id_field' => ++$i ];
46 } );
47 $db->expects( $this->exactly( count( $response ) ) )
48 ->method( 'select' )
49 ->will( $this->consecutivelyReturnFromSelect( $response ) );
50
51 $pos = 0;
52 foreach ( $reader as $rows ) {
53 $this->assertEquals( $response[$pos], $rows, "Testing row in position $pos" );
54 $pos++;
55 }
56 // -1 is because the final array() marks the end and isnt included
57 $this->assertEquals( count( $response ) - 1, $pos );
58 }
59
60 public static function provider_readerGetPrimaryKey() {
61 $row = [
62 'id_field' => 42,
63 'some_col' => 'dvorak',
64 'other_col' => 'samurai',
65 ];
66 return [
67
68 [
69 'Must return single column pk when requested',
70 [ 'id_field' => 42 ],
71 $row
72 ],
73
74 [
75 'Must return multiple column pks when requested',
76 [ 'id_field' => 42, 'other_col' => 'samurai' ],
77 $row
78 ],
79
80 ];
81 }
82
83 /**
84 * @dataProvider provider_readerGetPrimaryKey
85 */
86 public function testReaderGetPrimaryKey( $message, array $expected, array $row ) {
87 $reader = new BatchRowIterator( $this->mockDb(), 'some_table', array_keys( $expected ), 8675309 );
88 $this->assertEquals( $expected, $reader->extractPrimaryKeys( (object)$row ), $message );
89 }
90
91 public static function provider_readerSetFetchColumns() {
92 return [
93
94 [
95 'Must merge primary keys into select conditions',
96 // Expected column select
97 [ 'foo', 'bar' ],
98 // primary keys
99 [ 'foo' ],
100 // setFetchColumn
101 [ 'bar' ]
102 ],
103
104 [
105 'Must not merge primary keys into the all columns selector',
106 // Expected column select
107 [ '*' ],
108 // primary keys
109 [ 'foo' ],
110 // setFetchColumn
111 [ '*' ],
112 ],
113
114 [
115 'Must not duplicate primary keys into column selector',
116 // Expected column select.
117 // TODO: figure out how to only assert the array_values portion and not the keys
118 [ 0 => 'foo', 1 => 'bar', 3 => 'baz' ],
119 // primary keys
120 [ 'foo', 'bar', ],
121 // setFetchColumn
122 [ 'bar', 'baz' ],
123 ],
124 ];
125 }
126
127 /**
128 * @dataProvider provider_readerSetFetchColumns
129 */
130 public function testReaderSetFetchColumns(
131 $message, array $columns, array $primaryKeys, array $fetchColumns
132 ) {
133 $db = $this->mockDb();
134 $db->expects( $this->once() )
135 ->method( 'select' )
136 // only testing second parameter of Database::select
137 ->with( 'some_table', $columns )
138 ->will( $this->returnValue( new ArrayIterator( [] ) ) );
139
140 $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, 22 );
141 $reader->setFetchColumns( $fetchColumns );
142 // triggers first database select
143 $reader->rewind();
144 }
145
146 public static function provider_readerSelectConditions() {
147 return [
148
149 [
150 "With single primary key must generate id > 'value'",
151 // Expected second iteration
152 [ "( id_field > '3' )" ],
153 // Primary key(s)
154 'id_field',
155 ],
156
157 [
158 'With multiple primary keys the first conditions ' .
159 'must use >= and the final condition must use >',
160 // Expected second iteration
161 [ "( id_field = '3' AND foo > '103' ) OR ( id_field > '3' )" ],
162 // Primary key(s)
163 [ 'id_field', 'foo' ],
164 ],
165
166 ];
167 }
168
169 /**
170 * Slightly hackish to use reflection, but asserting different parameters
171 * to consecutive calls of Database::select in phpunit is error prone
172 *
173 * @dataProvider provider_readerSelectConditions
174 */
175 public function testReaderSelectConditionsMultiplePrimaryKeys(
176 $message, $expectedSecondIteration, $primaryKeys, $batchSize = 3
177 ) {
178 $results = $this->genSelectResult( $batchSize, $batchSize * 3, function () {
179 static $i = 0, $j = 100, $k = 1000;
180 return [ 'id_field' => ++$i, 'foo' => ++$j, 'bar' => ++$k ];
181 } );
182 $db = $this->mockDbConsecutiveSelect( $results );
183
184 $conditions = [ 'bar' => 42, 'baz' => 'hai' ];
185 $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, $batchSize );
186 $reader->addConditions( $conditions );
187
188 $buildConditions = new ReflectionMethod( $reader, 'buildConditions' );
189 $buildConditions->setAccessible( true );
190
191 // On first iteration only the passed conditions must be used
192 $this->assertEquals( $conditions, $buildConditions->invoke( $reader ),
193 'First iteration must return only the conditions passed in addConditions' );
194 $reader->rewind();
195
196 // Second iteration must use the maximum primary key of last set
197 $this->assertEquals(
198 $conditions + $expectedSecondIteration,
199 $buildConditions->invoke( $reader ),
200 $message
201 );
202 }
203
204 protected function mockDbConsecutiveSelect( array $retvals ) {
205 $db = $this->mockDb();
206 $db->expects( $this->any() )
207 ->method( 'select' )
208 ->will( $this->consecutivelyReturnFromSelect( $retvals ) );
209 $db->expects( $this->any() )
210 ->method( 'addQuotes' )
211 ->will( $this->returnCallback( function ( $value ) {
212 return "'$value'"; // not real quoting: doesn't matter in test
213 } ) );
214
215 return $db;
216 }
217
218 protected function consecutivelyReturnFromSelect( array $results ) {
219 $retvals = [];
220 foreach ( $results as $rows ) {
221 // The Database::select method returns iterators, so we do too.
222 $retvals[] = $this->returnValue( new ArrayIterator( $rows ) );
223 }
224
225 return call_user_func_array( [ $this, 'onConsecutiveCalls' ], $retvals );
226 }
227
228 protected function genSelectResult( $batchSize, $numRows, $rowGenerator ) {
229 $res = [];
230 for ( $i = 0; $i < $numRows; $i += $batchSize ) {
231 $rows = [];
232 for ( $j = 0; $j < $batchSize && $i + $j < $numRows; $j++ ) {
233 $rows [] = (object)call_user_func( $rowGenerator );
234 }
235 $res[] = $rows;
236 }
237 $res[] = []; // termination condition requires empty result for last row
238 return $res;
239 }
240
241 protected function mockDb() {
242 // @TODO: mock from Database
243 // FIXME: the constructor normally sets mAtomicLevels and mSrvCache
244 $databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
245 ->disableOriginalConstructor()
246 ->getMock();
247 $databaseMysql->expects( $this->any() )
248 ->method( 'isOpen' )
249 ->will( $this->returnValue( true ) );
250 $databaseMysql->expects( $this->any() )
251 ->method( 'getApproximateLagStatus' )
252 ->will( $this->returnValue( [ 'lag' => 0, 'since' => 0 ] ) );
253 return $databaseMysql;
254 }
255 }