Passed
Pull Request — master (#195)
by Marius
02:18
created

provideGetReferencedEntityIdNoError()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 113
Code Lines 98

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 113
rs 8.2857
c 0
b 0
f 0
cc 1
eloc 98
nc 1
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Wikibase\DataModel\Services\Tests\Lookup;
4
5
use DataValues\StringValue;
6
use PHPUnit_Framework_TestCase;
7
use Wikibase\DataModel\Entity\EntityId;
8
use Wikibase\DataModel\Entity\EntityIdValue;
9
use Wikibase\DataModel\Entity\Item;
10
use Wikibase\DataModel\Entity\ItemId;
11
use Wikibase\DataModel\Entity\PropertyId;
12
use Wikibase\DataModel\Services\Entity\EntityPrefetcher;
13
use Wikibase\DataModel\Services\Entity\NullEntityPrefetcher;
14
use Wikibase\DataModel\Services\Lookup\EntityLookup;
15
use Wikibase\DataModel\Services\Lookup\EntityLookupException;
16
use Wikibase\DataModel\Services\Lookup\EntityRetrievingClosestReferencedEntityIdLookup;
17
use Wikibase\DataModel\Services\Lookup\InMemoryEntityLookup;
18
use Wikibase\DataModel\Services\Lookup\MaxReferenceDepthExhaustedException;
19
use Wikibase\DataModel\Services\Lookup\MaxReferencedEntityVisitsExhaustedException;
20
use Wikibase\DataModel\Services\Lookup\ReferencedEntityIdLookupException;
21
use Wikibase\DataModel\Services\Lookup\RestrictedEntityLookup;
22
use Wikibase\DataModel\Snak\PropertyNoValueSnak;
23
use Wikibase\DataModel\Snak\PropertyValueSnak;
24
use Wikibase\DataModel\Statement\Statement;
25
use Wikibase\DataModel\Statement\StatementList;
26
27
/**
28
 * @covers Wikibase\DataModel\Services\Lookup\EntityRetrievingClosestReferencedEntityIdLookup
29
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
30
 *
31
 * @license GPL-2.0-or-later
32
 * @author Marius Hoch
33
 */
34
class EntityRetrievingClosestReferencedEntityIdLookupTest extends PHPUnit_Framework_TestCase {
35
36
	/**
37
	 * @param EntityLookup $entityLookup
38
	 * @param int $expectedNumberOfGetEntityCalls
39
	 * @return EntityLookup
40
	 */
41
	private function maskEntityLookup( EntityLookup $entityLookup, $expectedNumberOfGetEntityCalls ) {
42
		if ( $expectedNumberOfGetEntityCalls > 0 ) {
43
			$entityLookup = new RestrictedEntityLookup( $entityLookup, $expectedNumberOfGetEntityCalls );
44
		} else {
45
			$entityLookup = $this->getMock( EntityLookup::class );
46
			$entityLookup->expects( $this->never() )
47
				->method( 'getEntity' );
48
		}
49
50
		return $entityLookup;
51
	}
52
53
	/**
54
	 * @param int $expectedPrefetches
55
	 * @return EntityPrefetcher
56
	 */
57
	private function getEntityPrefetcher( $expectedPrefetches ) {
58
		$entityPrefetcher = $this->getMock( EntityPrefetcher::class );
59
		$entityPrefetcher->expects( $this->exactly( $expectedPrefetches ) )
60
			->method( 'prefetch' )
61
			->with( $this->isType( 'array' ) );
62
63
		return $entityPrefetcher;
64
	}
65
66
	/**
67
	 * @param PropertyId $via
68
	 * @param EntityId[] $to
69
	 *
70
	 * @return StatementList
71
	 */
72
	private function getReferencingStatementList( PropertyId $via, array $to ) {
73
		$statementList = new StatementList();
74
75
		foreach ( $to as $toId ) {
76
			$value = new EntityIdValue( $toId );
77
			$mainSnak = new PropertyValueSnak( $via, $value );
78
			$statementList->addStatement( new Statement( $mainSnak ) );
79
		}
80
81
		return $statementList;
82
	}
83
84
	/**
85
	 * @return InMemoryEntityLookup
86
	 */
87
	private function getReferencingEntityStructure() {
88
		// This returns the following entity structure (all entities linked by P599)
89
		// Q1 -> Q5 -> Q599 -> Q1234
90
		//   \             \
91
		//    \             -- Q12 -> Q404
92
		//     --- Q90 -> Q3
93
		// Note: Q404 doesn't exist
94
95
		$pSubclassOf = new PropertyId( 'P599' );
96
		$q1 = new ItemId( 'Q1' );
97
		$q5 = new ItemId( 'Q5' );
98
		$q599 = new ItemId( 'Q599' );
99
		$q12 = new ItemId( 'Q12' );
100
		$q404 = new ItemId( 'Q404' );
101
		$q1234 = new ItemId( 'Q1234' );
102
		$q90 = new ItemId( 'Q90' );
103
		$q3 = new ItemId( 'Q3' );
104
105
		$lookup = new InMemoryEntityLookup();
106
107
		$lookup->addEntity(
108
			new Item( $q1, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q5, $q90 ] ) )
109
		);
110
		$lookup->addEntity(
111
			new Item( $q5, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q599 ] ) )
112
		);
113
		$lookup->addEntity(
114
			new Item( $q599, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q12, $q1234 ] ) )
115
		);
116
		$lookup->addEntity(
117
			new Item( $q12, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q404 ] ) )
118
		);
119
		$lookup->addEntity(
120
			new Item( $q90, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q3 ] ) )
121
		);
122
		$lookup->addEntity( new Item( $q404, null, null, null ) );
123
		$lookup->addEntity( new Item( $q1234, null, null, null ) );
124
		$lookup->addEntity( new Item( $q3, null, null, null ) );
125
126
		return $lookup;
127
	}
128
129
	/**
130
	 * @return InMemoryEntityLookup
131
	 */
132
	private function getCircularReferencingEntityStructure() {
133
		// This returns the following entity structure (all entities linked by P599)
134
		// Q1 -> Q5 -> Q1 -> Q5 -> …
135
		//   \           \
136
		//    --- Q90     --- Q90
137
138
		$pSubclassOf = new PropertyId( 'P599' );
139
		$q1 = new ItemId( 'Q1' );
140
		$q5 = new ItemId( 'Q5' );
141
		$q90 = new ItemId( 'Q90' );
142
143
		$lookup = new InMemoryEntityLookup();
144
145
		$lookup->addEntity(
146
			new Item( $q1, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q5, $q90 ] ) )
147
		);
148
		$lookup->addEntity(
149
			new Item( $q5, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q1 ] ) )
150
		);
151
		$lookup->addEntity(
152
			new Item( $q90, null, null, null )
153
		);
154
155
		return $lookup;
156
	}
157
158
	/**
159
	 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
160
	 */
161
	public function provideGetReferencedEntityIdNoError() {
162
		$pSubclassOf = new PropertyId( 'P599' );
163
		$q1 = new ItemId( 'Q1' );
164
		$q3 = new ItemId( 'Q3' );
165
		$q5 = new ItemId( 'Q5' );
166
		$q12 = new ItemId( 'Q12' );
167
		$q403 = new ItemId( 'Q403' );
168
		$q404 = new ItemId( 'Q404' );
169
		$referencingEntityStructureLookup = $this->getReferencingEntityStructure();
170
		$circularReferencingEntityStructure = $this->getCircularReferencingEntityStructure();
171
172
		return [
173
			'empty list of target ids' => [
174
				null,
175
				0,
176
				0,
177
				$referencingEntityStructureLookup,
178
				$q1,
179
				$pSubclassOf,
180
				[]
181
			],
182
			'no such statement' => [
183
				null,
184
				1,
185
				0,
186
				$referencingEntityStructureLookup,
187
				$q1,
188
				new PropertyId( 'P12345' ),
189
				[ $q5 ]
190
			],
191
			'from id does not exist' => [
192
				null,
193
				1,
194
				0,
195
				$referencingEntityStructureLookup,
196
				$q404,
197
				$pSubclassOf,
198
				[ $q5 ]
199
			],
200
			'directly referenced entity #1' => [
201
				$q5,
202
				1,
203
				0,
204
				$referencingEntityStructureLookup,
205
				$q1,
206
				$pSubclassOf,
207
				[ $q5 ]
208
			],
209
			'directly referenced entity #2' => [
210
				$q1,
211
				1,
212
				0,
213
				$circularReferencingEntityStructure,
214
				$q5,
215
				$pSubclassOf,
216
				[ $q12, $q403, $q1, $q404 ]
217
			],
218
			'directly referenced entity, two target ids' => [
219
				$q5,
220
				1,
221
				0,
222
				$referencingEntityStructureLookup,
223
				$q1,
224
				$pSubclassOf,
225
				[ $q5, $q404 ]
226
			],
227
			'indirectly referenced entity #1' => [
228
				$q3,
229
				3,
230
				1,
231
				$referencingEntityStructureLookup,
232
				$q1,
233
				$pSubclassOf,
234
				[ $q3 ]
235
			],
236
			'indirectly referenced entity #2' => [
237
				$q12,
238
				4,
239
				2,
240
				$referencingEntityStructureLookup,
241
				$q1,
242
				$pSubclassOf,
243
				[ $q12 ]
244
			],
245
			'indirectly referenced entity, multiple target ids' => [
246
				$q12,
247
				4,
248
				2,
249
				$referencingEntityStructureLookup,
250
				$q1,
251
				$pSubclassOf,
252
				[ $q12, $q403, $q404 ]
253
			],
254
			'indirectly referenced entity, multiple target ids' => [
255
				$q12,
256
				4,
257
				2,
258
				$referencingEntityStructureLookup,
259
				$q1,
260
				$pSubclassOf,
261
				[ $q12, $q403, $q404 ]
262
			],
263
			'circular reference detection' => [
264
				null,
265
				3,
266
				1,
267
				$circularReferencingEntityStructure,
268
				$q1,
269
				$pSubclassOf,
270
				[ $q403, $q404 ]
271
			],
272
		];
273
	}
274
275
	/**
276
	 * @dataProvider provideGetReferencedEntityIdNoError
277
	 */
278
	public function testGetReferencedEntityIdNoError(
279
		EntityId $expectedToId = null,
280
		$maxEntityVisits,
281
		$maxDepth,
282
		EntityLookup $entityLookup,
283
		EntityId $fromId,
284
		PropertyId $propertyId,
285
		array $toIds
286
	) {
287
		// Number of prefetching operations to expect (Note: We call getReferencedEntityId twice)
288
		$expectedNumberOfPrefetches = $maxEntityVisits ? ( $maxDepth + 1 ) * 2 : 0;
289
290
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
291
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
292
			$this->getEntityPrefetcher( $expectedNumberOfPrefetches ),
293
			$maxDepth,
294
			$maxEntityVisits
295
		);
296
		$result = $lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
297
298
		$this->assertEquals( $expectedToId, $result );
299
300
		// Run again to see if the maxDepth/visitedEntityRelated state is properly resetted
301
		$this->assertEquals(
302
			$expectedToId,
303
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds )
304
		);
305
	}
306
307
	public function provideGetReferencedEntityIdMaxDepthExceeded() {
308
		$cases = $this->provideGetReferencedEntityIdNoError();
309
310
		foreach ( $cases as $caseName => $case ) {
311
			if ( end( $case ) === [] ) {
312
				// In case we search for nothing, the max depth can't ever be exceeded
313
				continue;
314
			}
315
316
			// Remove expected to id
317
			array_shift( $case );
318
			// Reduce max depth by 1
319
			$case[1]--;
320
321
			yield $caseName => $case;
322
		}
323
	}
324
325
	/**
326
	 * @dataProvider provideGetReferencedEntityIdMaxDepthExceeded
327
	 */
328
	public function testGetReferencedEntityIdMaxDepthExceeded(
329
		$maxEntityVisits,
330
		$maxDepth,
331
		EntityLookup $entityLookup,
332
		EntityId $fromId,
333
		PropertyId $propertyId,
334
		array $toIds
335
	) {
336
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
337
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
338
			new NullEntityPrefetcher(),
339
			$maxDepth,
340
			$maxEntityVisits
341
		);
342
343
		try {
344
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
345
		} catch ( MaxReferenceDepthExhaustedException $exception ) {
346
			$this->assertSame( $maxDepth, $exception->getMaxDepth() );
347
348
			return;
349
		}
350
		$this->fail( 'No expection thrown!' );
351
	}
352
353
	public function provideGetReferencedEntityIdMaxEntityVisitsExceeded() {
354
		$cases = $this->provideGetReferencedEntityIdNoError();
355
356
		foreach ( $cases as $caseName => $case ) {
357
			if ( end( $case ) === [] ) {
358
				// In case we search for nothing, no entity will ever be loaded
359
				continue;
360
			}
361
362
			// Remove expected to id
363
			array_shift( $case );
364
			// Reduce max entity visits by 1
365
			$case[0]--;
366
367
			yield $caseName => $case;
368
		}
369
	}
370
371
	/**
372
	 * @dataProvider provideGetReferencedEntityIdMaxEntityVisitsExceeded
373
	 */
374
	public function testGetReferencedEntityIdMaxEntityVisitsExceeded(
375
		$maxEntityVisits,
376
		$maxDepth,
377
		EntityLookup $entityLookup,
378
		EntityId $fromId,
379
		PropertyId $propertyId,
380
		array $toIds
381
	) {
382
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
383
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
384
			new NullEntityPrefetcher(),
385
			$maxDepth,
386
			$maxEntityVisits
387
		);
388
389
		try {
390
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
391
		} catch ( MaxReferencedEntityVisitsExhaustedException $exception ) {
392
			$this->assertSame( $maxEntityVisits, $exception->getMaxEntityVisits() );
393
394
			return;
395
		}
396
		$this->fail( 'No expection thrown!' );
397
	}
398
399
	public function provideGetReferencedEntityIdTestInvalidSnak() {
400
		$q42 = new ItemId( 'Q42' );
401
		$p1 = new PropertyId( 'P1' );
402
		$p2 = new PropertyId( 'P2' );
403
		$statementList = new StatementList();
404
405
		$statementList->addStatement(
406
			new Statement( new PropertyNoValueSnak( $p1 ) )
407
		);
408
409
		$statementList->addStatement(
410
			new Statement( new PropertyValueSnak( $p2, new StringValue( '12' ) ) )
411
		);
412
413
		$entityLookup = new InMemoryEntityLookup();
414
		$entityLookup->addEntity( new Item( $q42, null, null, $statementList ) );
415
416
		return [
417
			'no value snak' => [
418
				$entityLookup,
419
				$q42,
420
				$p1,
421
				[ $q42 ]
422
			],
423
			'wrong datatype' => [
424
				$entityLookup,
425
				$q42,
426
				$p2,
427
				[ $q42 ]
428
			],
429
		];
430
	}
431
432
	/**
433
	 * @dataProvider provideGetReferencedEntityIdTestInvalidSnak
434
	 */
435
	public function testGetReferencedEntityIdTestInvalidSnak(
436
		EntityLookup $entityLookup,
437
		EntityId $fromId,
438
		PropertyId $propertyId,
439
		array $toIds
440
	) {
441
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
442
			$this->maskEntityLookup( $entityLookup, 1 ),
443
			new NullEntityPrefetcher(),
444
			0,
445
			1
446
		);
447
448
		$this->assertNull(
449
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds )
450
		);
451
	}
452
453
	public function testGetReferencedEntityIdEntityLookupException() {
454
		$q2013 = new ItemId( 'Q2013' );
455
456
		$entityLookupException = new EntityLookupException( $q2013 );
457
		$entityLookup = new InMemoryEntityLookup();
458
		$entityLookup->addException( $entityLookupException );
459
460
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
461
			$entityLookup,
462
			new NullEntityPrefetcher(),
463
			50,
464
			50
465
		);
466
467
		try {
468
			$lookup->getReferencedEntityId( $q2013, new PropertyId( 'P31' ), [ new ItemId( 'Q154187' ) ] );
469
		} catch ( ReferencedEntityIdLookupException $exception ) {
470
			$this->assertInstanceOf( EntityLookupException::class, $exception->getPrevious() );
471
472
			return;
473
		}
474
		$this->fail( 'No expection thrown!' );
475
	}
476
477
}
478