ChangeRunCoalescerTest::assertChangeEquals()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 3
nc 2
nop 2
1
<?php
2
3
namespace Wikibase\Client\Tests\Integration\Changes;
4
5
use MediaWikiIntegrationTestCase;
6
use Psr\Log\NullLogger;
7
use Wikibase\Client\Changes\ChangeRunCoalescer;
8
use Wikibase\DataModel\Entity\Item;
9
use Wikibase\DataModel\Entity\ItemId;
10
use Wikibase\DataModel\Services\Diff\ItemDiffer;
11
use Wikibase\DataModel\SiteLink;
12
use Wikibase\Lib\Changes\Change;
13
use Wikibase\Lib\Changes\EntityChange;
14
use Wikibase\Lib\Changes\EntityDiffChangedAspectsFactory;
15
use Wikibase\Lib\Changes\ItemChange;
16
use Wikibase\Lib\Store\EntityRevisionLookup;
17
use Wikibase\Lib\Tests\Changes\TestChanges;
18
use Wikibase\Lib\Tests\MockRepository;
19
20
/**
21
 * @covers \Wikibase\Client\Changes\ChangeRunCoalescer
22
 *
23
 * @group Wikibase
24
 * @group WikibaseClient
25
 * @group WikibaseChange
26
 *
27
 * @group Database
28
 *
29
 * @license GPL-2.0-or-later
30
 * @author Daniel Kinzler
31
 */
32
class ChangeRunCoalescerTest extends MediaWikiIntegrationTestCase {
33
34
	private function getChangeRunCoalescer() {
35
		$entityRevisionLookup = $this->getEntityRevisionLookup();
36
		$changeFactory = TestChanges::getEntityChangeFactory();
37
38
		$coalescer = new ChangeRunCoalescer(
39
			$entityRevisionLookup,
40
			$changeFactory,
41
			new NullLogger(),
42
			'enwiki'
43
		);
44
45
		return $coalescer;
46
	}
47
48
	/**
49
	 * @return EntityRevisionLookup
50
	 */
51
	private function getEntityRevisionLookup() {
52
		$repo = new MockRepository();
0 ignored issues
show
Deprecated Code introduced by
The class Wikibase\Lib\Tests\MockRepository has been deprecated with message: Try to use a simpler fake. The complexity and coupling of this
test double are very high, so it is good to avoid binding to it. Mock repository for use in tests.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
53
54
		$offsets = [ 'Q1' => 1100, 'Q2' => 1200 ];
55
		foreach ( $offsets as $qid => $offset ) {
56
			// entity 1, revision 1111
57
			$entity1 = new Item( new ItemId( $qid ) );
58
			$entity1->setLabel( 'en', 'ORIGINAL' );
59
			$entity1->getSiteLinkList()->addNewSiteLink( 'enwiki', 'Original' );
60
			$repo->putEntity( $entity1, $offset + 11 );
61
62
			// entity 1, revision 1112
63
			$entity1->setLabel( 'de', 'HINZUGEFÜGT' );
64
			$repo->putEntity( $entity1, $offset + 12 );
65
66
			// entity 1, revision 1113
67
			$entity1->setLabel( 'nl', 'Addiert' );
68
			$repo->putEntity( $entity1, $offset + 13 );
69
70
			// entity 1, revision 1114
71
			$entity1->getSiteLinkList()->addNewSiteLink( 'dewiki', 'Testen' );
72
			$repo->putEntity( $entity1, $offset + 14 );
73
74
			// entity 1, revision 1115
75
			$entity1->getSiteLinkList()->setSiteLink( new SiteLink( 'enwiki', 'Original', [ new ItemId( 'Q12345' ) ] ) );
76
			$repo->putEntity( $entity1, $offset + 15 );
77
78
			// entity 1, revision 1117
79
			$entity1->getSiteLinkList()->setSiteLink( new SiteLink( 'enwiki', 'Spam', [ new ItemId( 'Q12345' ) ] ) );
80
			$repo->putEntity( $entity1, $offset + 17 );
81
82
			// entity 1, revision 1118
83
			$entity1->getSiteLinkList()->setSiteLink( new SiteLink( 'enwiki', 'Spam', [ new ItemId( 'Q54321' ) ] ) );
84
			$repo->putEntity( $entity1, $offset + 18 );
85
		}
86
87
		return $repo;
88
	}
89
90
	/**
91
	 * @param array $values
92
	 *
93
	 * @return EntityChange
94
	 */
95
	private function makeChange( array $values ) {
96
		if ( !isset( $values['info'] ) ) {
97
			$values['info'] = [];
98
		}
99
100
		if ( !isset( $values['info']['metadata'] ) ) {
101
			$values['info']['metadata'] = [];
102
		}
103
104
		if ( !isset( $values['info']['metadata']['rev_id'] ) && isset( $values['revision_id'] ) ) {
105
			$values['info']['metadata']['rev_id'] = $values[ 'revision_id' ];
106
		}
107
108
		if ( !isset( $values['info']['metadata']['user_text'] ) && isset( $values['user_id'] ) ) {
109
			$values['info']['metadata']['user_text'] = 'User' . $values['user_id'];
110
		}
111
112
		if ( !isset( $values['info']['metadata']['parent_id'] ) && isset( $values['parent_id'] ) ) {
113
			$values['info']['metadata']['parent_id'] = $values['parent_id'];
114
		}
115
116
		if ( !isset( $values['info']['metadata']['parent_id'] ) ) {
117
			$values['info']['metadata']['parent_id'] = 0;
118
		}
119
120
		if ( !isset( $values['info']['metadata']['comment'] ) && isset( $values['comment'] ) ) {
121
			$values['info']['metadata']['comment'] = $values['comment'];
122
		}
123
124
		if ( !isset( $values['info']['metadata']['comment'] ) ) {
125
			$values['info']['metadata']['comment'] = str_replace( '~', '-', $values['type'] );
126
		}
127
128
		$diff = $this->makeDiff( $values['object_id'], $values['info']['metadata']['parent_id'], $values[ 'revision_id' ] );
129
		$values['info'] = json_encode( $values['info'] );
130
131
		if ( $values['type'] === 'wikibase-item~add' || $values['type'] === 'wikibase-item~update' ) {
132
			$change = new ItemChange( $values );
133
		} else {
134
			$change = new EntityChange( $values );
135
		}
136
		$change->setEntityId( new ItemId( $values['object_id'] ) );
137
138
		$diffAspects = ( new EntityDiffChangedAspectsFactory() )->newFromEntityDiff( $diff );
139
		$change->setCompactDiff( $diffAspects );
140
141
		return $change;
142
	}
143
144
	private function combineChanges( EntityChange $first, EntityChange $last ) {
145
		$firstmeta = $first->getMetadata();
146
		$lastmeta = $last->getMetadata();
147
148
		return $this->makeChange( [
149
			'id' => null,
150
			'type' => $first->getField( 'type' ), // because the first change has no parent
151
			'time' => $last->getField( 'time' ), // last change's timestamp
152
			'object_id' => $last->getField( 'object_id' ),
153
			'revision_id' => $last->getField( 'revision_id' ), // last changes rev id
154
			'user_id' => $last->getField( 'user_id' ),
155
			'info' => [
156
				'metadata' => [
157
					'bot' => 0,
158
					'comment' => $lastmeta['comment'],
159
					'parent_id' => $firstmeta['parent_id'],
160
				]
161
			]
162
		] );
163
	}
164
165
	private function makeDiff( $objectId, $revA, $revB ) {
166
		$lookup = $this->getEntityRevisionLookup();
167
168
		$itemId = new ItemId( $objectId );
169
170
		if ( $revA === 0 ) {
171
			$oldEntity = new Item();
172
		} else {
173
			$oldEntity = $lookup->getEntityRevision( $itemId, $revA )->getEntity();
174
		}
175
176
		if ( $revB === 0 ) {
177
			$newEntity = new Item();
178
		} else {
179
			$newEntity = $lookup->getEntityRevision( $itemId, $revB )->getEntity();
180
		}
181
182
		$differ = new ItemDiffer();
183
		return $differ->diffEntities( $oldEntity, $newEntity );
184
	}
185
186
	private function assertChangeEquals( Change $expected, Change $actual ) {
187
		$this->assertEquals( get_class( $expected ), get_class( $actual ), 'change.class' );
188
189
		$this->assertEquals( $expected->getObjectId(), $actual->getObjectId(), 'change.ObjectId' );
190
		$this->assertEquals( $expected->getTime(), $actual->getTime(), 'change.Time' );
191
		$this->assertEquals( $expected->getType(), $actual->getType(), 'change.Type' );
192
193
		if ( $expected instanceof EntityChange && $actual instanceof EntityChange ) {
194
			$this->assertEquals( $expected->getAction(), $actual->getAction(), 'change.Action' );
195
			$this->assertArrayEquals( $expected->getMetadata(), $actual->getMetadata(), false, true );
196
		}
197
198
		$this->assertSame(
199
			$expected->getCompactDiff()->toArray(),
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Wikibase\Lib\Changes\Change as the method getCompactDiff() does only exist in the following implementations of said interface: Wikibase\Lib\Changes\DiffChange, Wikibase\Lib\Changes\EntityChange, Wikibase\Lib\Changes\ItemChange, Wikibase\Repo\Notifications\RepoEntityChange, Wikibase\Repo\Notifications\RepoItemChange.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
200
			$actual->getCompactDiff()->toArray()
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Wikibase\Lib\Changes\Change as the method getCompactDiff() does only exist in the following implementations of said interface: Wikibase\Lib\Changes\DiffChange, Wikibase\Lib\Changes\EntityChange, Wikibase\Lib\Changes\ItemChange, Wikibase\Repo\Notifications\RepoEntityChange, Wikibase\Repo\Notifications\RepoItemChange.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
201
		);
202
	}
203
204
	public function provideCoalesceChanges() {
205
		$id = 0;
206
207
		// create with a label and site link set
208
		$create11 = $this->makeChange( [
209
			'id' => ++$id,
210
			'type' => 'wikibase-item~add',
211
			'time' => '20130101010101',
212
			'object_id' => 'Q1',
213
			'revision_id' => 1111,
214
			'user_id' => 1,
215
		] );
216
217
		// set a label
218
		$update11 = $this->makeChange( [
219
			'id' => ++$id,
220
			'type' => 'wikibase-item~update',
221
			'time' => '20130102020202',
222
			'object_id' => 'Q1',
223
			'revision_id' => 1112,
224
			'user_id' => 1,
225
			'parent_id' => 1111,
226
		] );
227
228
		// set another label
229
		$anotherUpdate11 = $this->makeChange( [
230
			'id' => ++$id,
231
			'type' => 'wikibase-item~update',
232
			'time' => '20130102020203',
233
			'object_id' => 'Q1',
234
			'revision_id' => 1113,
235
			'user_id' => 1,
236
			'parent_id' => 1112,
237
		] );
238
239
		// set another label, by another user
240
		$anotherUpdate21 = $this->makeChange( [
241
			'id' => ++$id,
242
			'type' => 'wikibase-item~update',
243
			'time' => '20130102020203',
244
			'object_id' => 'Q1',
245
			'revision_id' => 1113,
246
			'user_id' => 2,
247
			'parent_id' => 1112,
248
		] );
249
250
		// change link to other wiki
251
		$update11XLink = $this->makeChange( [
252
			'id' => ++$id,
253
			'type' => 'wikibase-item~update',
254
			'time' => '20130101020304',
255
			'object_id' => 'Q1',
256
			'revision_id' => 1114,
257
			'user_id' => 1,
258
			'parent_id' => 1113,
259
		] );
260
261
		// change link to other wiki
262
		$update11Badge = $this->makeChange( [
263
			'id' => ++$id,
264
			'type' => 'wikibase-item~update',
265
			'time' => '20130101020305',
266
			'object_id' => 'Q1',
267
			'revision_id' => 1115,
268
			'user_id' => 1,
269
			'parent_id' => 1114,
270
		] );
271
272
		// change link to local wiki
273
		$update11Link = $this->makeChange( [
274
			'id' => ++$id,
275
			'type' => 'wikibase-item~update',
276
			'time' => '20130102030407',
277
			'object_id' => 'Q1',
278
			'revision_id' => 1117,
279
			'user_id' => 1,
280
			'parent_id' => 1115,
281
		] );
282
283
		// delete
284
		$delete11 = $this->makeChange( [
285
			'id' => ++$id,
286
			'type' => 'wikibase-item~remove',
287
			'time' => '20130102030409',
288
			'object_id' => 'Q1',
289
			'revision_id' => 0,
290
			'user_id' => 1,
291
			'parent_id' => 1118,
292
		] );
293
294
		// set a label
295
		$update12 = $this->makeChange( [
296
			'id' => ++$id,
297
			'type' => 'wikibase-item~update',
298
			'time' => '20130102020102',
299
			'object_id' => 'Q2',
300
			'revision_id' => 1212,
301
			'user_id' => 1,
302
			'parent_id' => 1211,
303
		] );
304
305
		// set another label
306
		$anotherUpdate12 = $this->makeChange( [
307
			'id' => ++$id,
308
			'type' => 'wikibase-item~update',
309
			'time' => '20130102020303',
310
			'object_id' => 'Q2',
311
			'revision_id' => 1213,
312
			'user_id' => 1,
313
			'parent_id' => 1213,
314
		] );
315
316
		return [
317
			'empty' => [
318
				[], // $changes
319
				[], // $expected
320
			],
321
322
			'single' => [
323
				[ $create11 ], // $changes
324
				[ $create11 ], // $expected
325
			],
326
327
			'simple run' => [
328
				[ $update11, $anotherUpdate11 ], // $changes
329
				[ $this->combineChanges( $update11, $anotherUpdate11 ) ], // $expected
330
			],
331
332
			'long run' => [ // create counts as update, delete doesn't
333
				[ $create11, $update11, $anotherUpdate11 ], // $changes
334
				[ $this->combineChanges( $create11, $anotherUpdate11 ) ], // $expected
335
			],
336
337
			'different items' => [
338
				[ $update11, $anotherUpdate12 ], // $changes
339
				[ $update11, $anotherUpdate12 ], // $changes
340
			],
341
342
			'different users' => [
343
				[ $update11, $anotherUpdate21 ], // $changes
344
				[ $update11, $anotherUpdate21 ], // $changes
345
			],
346
347
			'reversed' => [ // result is sorted by timestamp
348
				[ $update12, $create11 ], // $changes
349
				[ $create11, $update12 ], // $expected
350
			],
351
352
			'mingled' => [
353
				[ $update12, $update11, $anotherUpdate11, $anotherUpdate12 ], // $changes
354
				[ // result is sorted by timestamp
355
					$this->combineChanges( $update11, $anotherUpdate11 ),
356
					$this->combineChanges( $update12, $anotherUpdate12 ),
357
				], // $expected
358
			],
359
360
			'different action' => [ // create counts as update, delete doesn't
361
				[ $update11, $delete11 ], // $changes
362
				[ $update11, $delete11 ], // $expected
363
			],
364
365
			'local link breaks' => [
366
				[ $update11, $update11Link ], // $changes
367
				[ $update11, $update11Link ], // $expected
368
			],
369
370
			'local link badge change' => [
371
				[ $update11, $update11Badge ], // $changes
372
				[ $this->combineChanges( $update11, $update11Badge ) ], // $expected
373
			],
374
375
			'other link merges' => [
376
				[ $update11, $update11XLink ], // $changes
377
				[ $this->combineChanges( $update11, $update11XLink ) ], // $expected
378
			],
379
		];
380
	}
381
382
	/**
383
	 * @dataProvider provideCoalesceChanges
384
	 */
385
	public function testCoalesceChanges( $changes, $expected ) {
386
		$coalescer = $this->getChangeRunCoalescer();
387
		$coalesced = $coalescer->transformChangeList( $changes );
388
389
		$this->assertEquals( $this->getChangeIds( $expected ), $this->getChangeIds( $coalesced ) );
390
391
		// We know the arrays have the same length, but know nothing about they keys.
392
		$expected = array_values( $expected );
393
		$coalesced = array_values( $coalesced );
394
395
		foreach ( $expected as $i => $expectedChange ) {
396
			$actualChange = $coalesced[$i];
397
			$this->assertChangeEquals( $expectedChange, $actualChange );
398
		}
399
	}
400
401
	private function getChangeIds( array $changes ) {
402
		return array_map(
403
			function( Change $change ) {
404
				return $change->getId();
405
			},
406
			$changes
407
		);
408
	}
409
410
}
411