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