Fix 'Tags' padding to keep it farther from the edge and document the source of the...
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / SlotRecordTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use InvalidArgumentException;
6 use LogicException;
7 use MediaWiki\Storage\IncompleteRevisionException;
8 use MediaWiki\Storage\SlotRecord;
9 use MediaWiki\Storage\SuppressedDataException;
10 use MediaWikiTestCase;
11 use WikitextContent;
12
13 /**
14 * @covers \MediaWiki\Storage\SlotRecord
15 */
16 class SlotRecordTest extends MediaWikiTestCase {
17
18 private function makeRow( $data = [] ) {
19 $data = $data + [
20 'slot_id' => 1234,
21 'slot_content_id' => 33,
22 'content_size' => '5',
23 'content_sha1' => 'someHash',
24 'content_address' => 'tt:456',
25 'model_name' => CONTENT_MODEL_WIKITEXT,
26 'format_name' => CONTENT_FORMAT_WIKITEXT,
27 'slot_revision_id' => '2',
28 'slot_origin' => '1',
29 'role_name' => 'myRole',
30 ];
31 return (object)$data;
32 }
33
34 public function testCompleteConstruction() {
35 $row = $this->makeRow();
36 $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
37
38 $this->assertTrue( $record->hasAddress() );
39 $this->assertTrue( $record->hasRevision() );
40 $this->assertTrue( $record->isInherited() );
41 $this->assertSame( 'A', $record->getContent()->getNativeData() );
42 $this->assertSame( 5, $record->getSize() );
43 $this->assertSame( 'someHash', $record->getSha1() );
44 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
45 $this->assertSame( 2, $record->getRevision() );
46 $this->assertSame( 1, $record->getOrigin() );
47 $this->assertSame( 'tt:456', $record->getAddress() );
48 $this->assertSame( 33, $record->getContentId() );
49 $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
50 $this->assertSame( 'myRole', $record->getRole() );
51 }
52
53 public function testConstructionDeferred() {
54 $row = $this->makeRow( [
55 'content_size' => null, // to be computed
56 'content_sha1' => null, // to be computed
57 'format_name' => function () {
58 return CONTENT_FORMAT_WIKITEXT;
59 },
60 'slot_revision_id' => '2',
61 'slot_origin' => '2',
62 ] );
63
64 $content = function () {
65 return new WikitextContent( 'A' );
66 };
67
68 $record = new SlotRecord( $row, $content );
69
70 $this->assertTrue( $record->hasAddress() );
71 $this->assertTrue( $record->hasRevision() );
72 $this->assertFalse( $record->isInherited() );
73 $this->assertSame( 'A', $record->getContent()->getNativeData() );
74 $this->assertSame( 1, $record->getSize() );
75 $this->assertNotNull( $record->getSha1() );
76 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
77 $this->assertSame( 2, $record->getRevision() );
78 $this->assertSame( 2, $record->getRevision() );
79 $this->assertSame( 'tt:456', $record->getAddress() );
80 $this->assertSame( 33, $record->getContentId() );
81 $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
82 $this->assertSame( 'myRole', $record->getRole() );
83 }
84
85 public function testNewUnsaved() {
86 $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
87
88 $this->assertFalse( $record->hasAddress() );
89 $this->assertFalse( $record->hasRevision() );
90 $this->assertFalse( $record->isInherited() );
91 $this->assertSame( 'A', $record->getContent()->getNativeData() );
92 $this->assertSame( 1, $record->getSize() );
93 $this->assertNotNull( $record->getSha1() );
94 $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
95 $this->assertSame( 'myRole', $record->getRole() );
96 }
97
98 public function provideInvalidConstruction() {
99 yield 'both null' => [ null, null ];
100 yield 'null row' => [ null, new WikitextContent( 'A' ) ];
101 yield 'array row' => [ [], new WikitextContent( 'A' ) ];
102 yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
103 yield 'null content' => [ (object)[], null ];
104 }
105
106 /**
107 * @dataProvider provideInvalidConstruction
108 */
109 public function testInvalidConstruction( $row, $content ) {
110 $this->setExpectedException( InvalidArgumentException::class );
111 new SlotRecord( $row, $content );
112 }
113
114 public function testGetContentId_fails() {
115 $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
116 $this->setExpectedException( IncompleteRevisionException::class );
117
118 $record->getContentId();
119 }
120
121 public function testGetAddress_fails() {
122 $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
123 $this->setExpectedException( IncompleteRevisionException::class );
124
125 $record->getAddress();
126 }
127
128 public function provideIncomplete() {
129 $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
130 yield 'unsaved' => [ $unsaved ];
131
132 $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
133 $inherited = SlotRecord::newInherited( $parent );
134 yield 'inherited' => [ $inherited ];
135 }
136
137 /**
138 * @dataProvider provideIncomplete
139 */
140 public function testGetRevision_fails( SlotRecord $record ) {
141 $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
142 $this->setExpectedException( IncompleteRevisionException::class );
143
144 $record->getRevision();
145 }
146
147 /**
148 * @dataProvider provideIncomplete
149 */
150 public function testGetOrigin_fails( SlotRecord $record ) {
151 $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
152 $this->setExpectedException( IncompleteRevisionException::class );
153
154 $record->getOrigin();
155 }
156
157 public function provideHashStability() {
158 yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
159 yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
160 }
161
162 /**
163 * @dataProvider provideHashStability
164 */
165 public function testHashStability( $text, $hash ) {
166 // Changing the output of the hash function will break things horribly!
167
168 $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
169
170 $record = SlotRecord::newUnsaved( 'main', new WikitextContent( $text ) );
171 $this->assertSame( $hash, $record->getSha1() );
172 }
173
174 public function testNewWithSuppressedContent() {
175 $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
176 $output = SlotRecord::newWithSuppressedContent( $input );
177
178 $this->setExpectedException( SuppressedDataException::class );
179 $output->getContent();
180 }
181
182 public function testNewInherited() {
183 $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
184 $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
185
186 // This would happen while doing an edit, before saving revision meta-data.
187 $inherited = SlotRecord::newInherited( $parent );
188
189 $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
190 $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
191 $this->assertSame( $parent->getContent(), $inherited->getContent() );
192 $this->assertTrue( $inherited->isInherited() );
193 $this->assertFalse( $inherited->hasRevision() );
194
195 // make sure we didn't mess with the internal state of $parent
196 $this->assertFalse( $parent->isInherited() );
197 $this->assertSame( 7, $parent->getRevision() );
198
199 // This would happen while doing an edit, after saving the revision meta-data
200 // and content meta-data.
201 $saved = SlotRecord::newSaved(
202 10,
203 $inherited->getContentId(),
204 $inherited->getAddress(),
205 $inherited
206 );
207 $this->assertSame( $parent->getContentId(), $saved->getContentId() );
208 $this->assertSame( $parent->getAddress(), $saved->getAddress() );
209 $this->assertSame( $parent->getContent(), $saved->getContent() );
210 $this->assertTrue( $saved->isInherited() );
211 $this->assertTrue( $saved->hasRevision() );
212 $this->assertSame( 10, $saved->getRevision() );
213
214 // make sure we didn't mess with the internal state of $parent or $inherited
215 $this->assertSame( 7, $parent->getRevision() );
216 $this->assertFalse( $inherited->hasRevision() );
217 }
218
219 public function testNewSaved() {
220 // This would happen while doing an edit, before saving revision meta-data.
221 $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
222
223 // This would happen while doing an edit, after saving the revision meta-data
224 // and content meta-data.
225 $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
226 $this->assertFalse( $saved->isInherited() );
227 $this->assertTrue( $saved->hasRevision() );
228 $this->assertTrue( $saved->hasAddress() );
229 $this->assertSame( 'theNewAddress', $saved->getAddress() );
230 $this->assertSame( 20, $saved->getContentId() );
231 $this->assertSame( 'A', $saved->getContent()->getNativeData() );
232 $this->assertSame( 10, $saved->getRevision() );
233 $this->assertSame( 10, $saved->getOrigin() );
234
235 // make sure we didn't mess with the internal state of $unsaved
236 $this->assertFalse( $unsaved->hasAddress() );
237 $this->assertFalse( $unsaved->hasRevision() );
238 }
239
240 public function provideNewSaved_LogicException() {
241 $freshRow = $this->makeRow( [
242 'content_id' => 10,
243 'content_address' => 'address:1',
244 'slot_origin' => 1,
245 'slot_revision_id' => 1,
246 ] );
247
248 $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
249 yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
250 yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
251 yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
252
253 $inheritedRow = $this->makeRow( [
254 'content_id' => null,
255 'content_address' => null,
256 'slot_origin' => 0,
257 'slot_revision_id' => 1,
258 ] );
259
260 $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
261 yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
262 }
263
264 /**
265 * @dataProvider provideNewSaved_LogicException
266 */
267 public function testNewSaved_LogicException(
268 $revisionId,
269 $contentId,
270 $contentAddress,
271 SlotRecord $protoSlot
272 ) {
273 $this->setExpectedException( LogicException::class );
274 SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
275 }
276
277 public function provideNewSaved_InvalidArgumentException() {
278 $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
279
280 yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
281 yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
282 yield 'bad content address' => [ 7, 5, 77, $unsaved ];
283 }
284
285 /**
286 * @dataProvider provideNewSaved_InvalidArgumentException
287 */
288 public function testNewSaved_InvalidArgumentException(
289 $revisionId,
290 $contentId,
291 $contentAddress,
292 SlotRecord $protoSlot
293 ) {
294 $this->setExpectedException( InvalidArgumentException::class );
295 SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
296 }
297
298 public function provideHasSameContent() {
299 $fail = function () {
300 self::fail( 'There should be no need to actually load the content.' );
301 };
302
303 $a100a1 = new SlotRecord(
304 $this->makeRow(
305 [
306 'model_name' => 'A',
307 'content_size' => 100,
308 'content_sha1' => 'hash-a',
309 'content_address' => 'xxx:a1',
310 ]
311 ),
312 $fail
313 );
314 $a100a1b = new SlotRecord(
315 $this->makeRow(
316 [
317 'model_name' => 'A',
318 'content_size' => 100,
319 'content_sha1' => 'hash-a',
320 'content_address' => 'xxx:a1',
321 ]
322 ),
323 $fail
324 );
325 $a100null = new SlotRecord(
326 $this->makeRow(
327 [
328 'model_name' => 'A',
329 'content_size' => 100,
330 'content_sha1' => 'hash-a',
331 'content_address' => null,
332 ]
333 ),
334 $fail
335 );
336 $a100a2 = new SlotRecord(
337 $this->makeRow(
338 [
339 'model_name' => 'A',
340 'content_size' => 100,
341 'content_sha1' => 'hash-a',
342 'content_address' => 'xxx:a2',
343 ]
344 ),
345 $fail
346 );
347 $b100a1 = new SlotRecord(
348 $this->makeRow(
349 [
350 'model_name' => 'B',
351 'content_size' => 100,
352 'content_sha1' => 'hash-a',
353 'content_address' => 'xxx:a1',
354 ]
355 ),
356 $fail
357 );
358 $a200a1 = new SlotRecord(
359 $this->makeRow(
360 [
361 'model_name' => 'A',
362 'content_size' => 200,
363 'content_sha1' => 'hash-a',
364 'content_address' => 'xxx:a2',
365 ]
366 ),
367 $fail
368 );
369 $a100x1 = new SlotRecord(
370 $this->makeRow(
371 [
372 'model_name' => 'A',
373 'content_size' => 100,
374 'content_sha1' => 'hash-x',
375 'content_address' => 'xxx:x1',
376 ]
377 ),
378 $fail
379 );
380
381 yield 'same instance' => [ $a100a1, $a100a1, true ];
382 yield 'no address' => [ $a100a1, $a100null, true ];
383 yield 'same address' => [ $a100a1, $a100a1b, true ];
384 yield 'different address' => [ $a100a1, $a100a2, true ];
385 yield 'different model' => [ $a100a1, $b100a1, false ];
386 yield 'different size' => [ $a100a1, $a200a1, false ];
387 yield 'different hash' => [ $a100a1, $a100x1, false ];
388 }
389
390 /**
391 * @dataProvider provideHasSameContent
392 */
393 public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
394 $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
395 $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
396 }
397
398 }