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

EntityRetrievingClosestReferencedEntityIdLookupTest   B

Complexity

Total Complexity 24

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 18

Importance

Changes 0
Metric Value
wmc 24
lcom 2
cbo 18
dl 0
loc 443
rs 7.3333
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A maskEntityLookup() 0 11 2
A getEntityPrefetcher() 0 8 1
A getReferencingStatementList() 0 11 2
B getReferencingEntityStructure() 0 40 1
B getCircularReferencingEntityStructure() 0 25 1
B provideGetReferencedEntityIdNoError() 0 113 1
B testGetReferencedEntityIdNoError() 0 28 2
A provideGetReferencedEntityIdMaxDepthExceeded() 0 17 3
B testGetReferencedEntityIdMaxDepthExceeded() 0 24 2
A provideGetReferencedEntityIdMaxEntityVisitsExceeded() 0 17 3
B testGetReferencedEntityIdMaxEntityVisitsExceeded() 0 24 2
B provideGetReferencedEntityIdTestInvalidSnak() 0 32 1
A testGetReferencedEntityIdTestInvalidSnak() 0 17 1
A testGetReferencedEntityIdEntityLookupException() 0 23 2
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( $q1234, null, null, null ) );
123
		$lookup->addEntity( new Item( $q3, null, null, null ) );
124
125
		return $lookup;
126
	}
127
128
	/**
129
	 * @return InMemoryEntityLookup
130
	 */
131
	private function getCircularReferencingEntityStructure() {
132
		// This returns the following entity structure (all entities linked by P599)
133
		// Q1 -> Q5 -> Q1 -> Q5 -> …
134
		//   \           \
135
		//    --- Q90     --- Q90
136
137
		$pSubclassOf = new PropertyId( 'P599' );
138
		$q1 = new ItemId( 'Q1' );
139
		$q5 = new ItemId( 'Q5' );
140
		$q90 = new ItemId( 'Q90' );
141
142
		$lookup = new InMemoryEntityLookup();
143
144
		$lookup->addEntity(
145
			new Item( $q1, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q5, $q90 ] ) )
146
		);
147
		$lookup->addEntity(
148
			new Item( $q5, null, null, $this->getReferencingStatementList( $pSubclassOf, [ $q1 ] ) )
149
		);
150
		$lookup->addEntity(
151
			new Item( $q90, null, null, null )
152
		);
153
154
		return $lookup;
155
	}
156
157
	/**
158
	 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
159
	 */
160
	public function provideGetReferencedEntityIdNoError() {
161
		$pSubclassOf = new PropertyId( 'P599' );
162
		$q1 = new ItemId( 'Q1' );
163
		$q3 = new ItemId( 'Q3' );
164
		$q5 = new ItemId( 'Q5' );
165
		$q12 = new ItemId( 'Q12' );
166
		$q403 = new ItemId( 'Q403' );
167
		$q404 = new ItemId( 'Q404' );
168
		$referencingEntityStructureLookup = $this->getReferencingEntityStructure();
169
		$circularReferencingEntityStructure = $this->getCircularReferencingEntityStructure();
170
171
		return [
172
			'empty list of target ids' => [
173
				null,
174
				0,
175
				0,
176
				$referencingEntityStructureLookup,
177
				$q1,
178
				$pSubclassOf,
179
				[]
180
			],
181
			'no such statement' => [
182
				null,
183
				1,
184
				0,
185
				$referencingEntityStructureLookup,
186
				$q1,
187
				new PropertyId( 'P12345' ),
188
				[ $q5 ]
189
			],
190
			'from id does not exist' => [
191
				null,
192
				1,
193
				0,
194
				$referencingEntityStructureLookup,
195
				$q404,
196
				$pSubclassOf,
197
				[ $q5 ]
198
			],
199
			'directly referenced entity #1' => [
200
				$q5,
201
				1,
202
				0,
203
				$referencingEntityStructureLookup,
204
				$q1,
205
				$pSubclassOf,
206
				[ $q5 ]
207
			],
208
			'directly referenced entity #2' => [
209
				$q1,
210
				1,
211
				0,
212
				$circularReferencingEntityStructure,
213
				$q5,
214
				$pSubclassOf,
215
				[ $q12, $q403, $q1, $q404 ]
216
			],
217
			'directly referenced entity, two target ids' => [
218
				$q5,
219
				1,
220
				0,
221
				$referencingEntityStructureLookup,
222
				$q1,
223
				$pSubclassOf,
224
				[ $q5, $q404 ]
225
			],
226
			'indirectly referenced entity #1' => [
227
				$q3,
228
				3,
229
				1,
230
				$referencingEntityStructureLookup,
231
				$q1,
232
				$pSubclassOf,
233
				[ $q3 ]
234
			],
235
			'indirectly referenced entity #2' => [
236
				$q12,
237
				4,
238
				2,
239
				$referencingEntityStructureLookup,
240
				$q1,
241
				$pSubclassOf,
242
				[ $q12 ]
243
			],
244
			'indirectly referenced entity, multiple target ids' => [
245
				$q12,
246
				4,
247
				2,
248
				$referencingEntityStructureLookup,
249
				$q1,
250
				$pSubclassOf,
251
				[ $q12, $q403, $q404 ]
252
			],
253
			'indirectly referenced entity, multiple target ids' => [
254
				$q12,
255
				4,
256
				2,
257
				$referencingEntityStructureLookup,
258
				$q1,
259
				$pSubclassOf,
260
				[ $q12, $q403, $q404 ]
261
			],
262
			'circular reference detection' => [
263
				null,
264
				3,
265
				1,
266
				$circularReferencingEntityStructure,
267
				$q1,
268
				$pSubclassOf,
269
				[ $q403, $q404 ]
270
			],
271
		];
272
	}
273
274
	/**
275
	 * @dataProvider provideGetReferencedEntityIdNoError
276
	 */
277
	public function testGetReferencedEntityIdNoError(
278
		EntityId $expectedToId = null,
279
		$maxEntityVisits,
280
		$maxDepth,
281
		EntityLookup $entityLookup,
282
		EntityId $fromId,
283
		PropertyId $propertyId,
284
		array $toIds
285
	) {
286
		// Number of prefetching operations to expect (Note: We call getReferencedEntityId twice)
287
		$expectedNumberOfPrefetches = $maxEntityVisits ? ( $maxDepth + 1 ) * 2 : 0;
288
289
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
290
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
291
			$this->getEntityPrefetcher( $expectedNumberOfPrefetches ),
292
			$maxDepth,
293
			$maxEntityVisits
294
		);
295
		$result = $lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
296
297
		$this->assertEquals( $expectedToId, $result );
298
299
		// Run again to see if the maxDepth/visitedEntityRelated state is properly resetted
300
		$this->assertEquals(
301
			$expectedToId,
302
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds )
303
		);
304
	}
305
306
	public function provideGetReferencedEntityIdMaxDepthExceeded() {
307
		$cases = $this->provideGetReferencedEntityIdNoError();
308
309
		foreach ( $cases as $caseName => $case ) {
310
			if ( end( $case ) === [] ) {
311
				// In case we search for nothing, the max depth can't ever be exceeded
312
				continue;
313
			}
314
315
			// Remove expected to id
316
			array_shift( $case );
317
			// Reduce max depth by 1
318
			$case[1]--;
319
320
			yield $caseName => $case;
321
		}
322
	}
323
324
	/**
325
	 * @dataProvider provideGetReferencedEntityIdMaxDepthExceeded
326
	 */
327
	public function testGetReferencedEntityIdMaxDepthExceeded(
328
		$maxEntityVisits,
329
		$maxDepth,
330
		EntityLookup $entityLookup,
331
		EntityId $fromId,
332
		PropertyId $propertyId,
333
		array $toIds
334
	) {
335
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
336
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
337
			new NullEntityPrefetcher(),
338
			$maxDepth,
339
			$maxEntityVisits
340
		);
341
342
		try {
343
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
344
		} catch ( MaxReferenceDepthExhaustedException $exception ) {
345
			$this->assertSame( $maxDepth, $exception->getMaxDepth() );
346
347
			return;
348
		}
349
		$this->fail( 'No expection thrown!' );
350
	}
351
352
	public function provideGetReferencedEntityIdMaxEntityVisitsExceeded() {
353
		$cases = $this->provideGetReferencedEntityIdNoError();
354
355
		foreach ( $cases as $caseName => $case ) {
356
			if ( end( $case ) === [] ) {
357
				// In case we search for nothing, no entity will ever be loaded
358
				continue;
359
			}
360
361
			// Remove expected to id
362
			array_shift( $case );
363
			// Reduce max entity visits by 1
364
			$case[0]--;
365
366
			yield $caseName => $case;
367
		}
368
	}
369
370
	/**
371
	 * @dataProvider provideGetReferencedEntityIdMaxEntityVisitsExceeded
372
	 */
373
	public function testGetReferencedEntityIdMaxEntityVisitsExceeded(
374
		$maxEntityVisits,
375
		$maxDepth,
376
		EntityLookup $entityLookup,
377
		EntityId $fromId,
378
		PropertyId $propertyId,
379
		array $toIds
380
	) {
381
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
382
			$this->maskEntityLookup( $entityLookup, $maxEntityVisits ),
383
			new NullEntityPrefetcher(),
384
			$maxDepth,
385
			$maxEntityVisits
386
		);
387
388
		try {
389
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds );
390
		} catch ( MaxReferencedEntityVisitsExhaustedException $exception ) {
391
			$this->assertSame( $maxEntityVisits, $exception->getMaxEntityVisits() );
392
393
			return;
394
		}
395
		$this->fail( 'No expection thrown!' );
396
	}
397
398
	public function provideGetReferencedEntityIdTestInvalidSnak() {
399
		$q42 = new ItemId( 'Q42' );
400
		$p1 = new PropertyId( 'P1' );
401
		$p2 = new PropertyId( 'P2' );
402
		$statementList = new StatementList();
403
404
		$statementList->addStatement(
405
			new Statement( new PropertyNoValueSnak( $p1 ) )
406
		);
407
408
		$statementList->addStatement(
409
			new Statement( new PropertyValueSnak( $p2, new StringValue( '12' ) ) )
410
		);
411
412
		$entityLookup = new InMemoryEntityLookup();
413
		$entityLookup->addEntity( new Item( $q42, null, null, $statementList ) );
414
415
		return [
416
			'no value snak' => [
417
				$entityLookup,
418
				$q42,
419
				$p1,
420
				[ $q42 ]
421
			],
422
			'wrong datatype' => [
423
				$entityLookup,
424
				$q42,
425
				$p2,
426
				[ $q42 ]
427
			],
428
		];
429
	}
430
431
	/**
432
	 * @dataProvider provideGetReferencedEntityIdTestInvalidSnak
433
	 */
434
	public function testGetReferencedEntityIdTestInvalidSnak(
435
		EntityLookup $entityLookup,
436
		EntityId $fromId,
437
		PropertyId $propertyId,
438
		array $toIds
439
	) {
440
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
441
			$this->maskEntityLookup( $entityLookup, 1 ),
442
			new NullEntityPrefetcher(),
443
			0,
444
			1
445
		);
446
447
		$this->assertNull(
448
			$lookup->getReferencedEntityId( $fromId, $propertyId, $toIds )
449
		);
450
	}
451
452
	public function testGetReferencedEntityIdEntityLookupException() {
453
		$q2013 = new ItemId( 'Q2013' );
454
455
		$entityLookupException = new EntityLookupException( $q2013 );
456
		$entityLookup = new InMemoryEntityLookup();
457
		$entityLookup->addException( $entityLookupException );
458
459
		$lookup = new EntityRetrievingClosestReferencedEntityIdLookup(
460
			$entityLookup,
461
			new NullEntityPrefetcher(),
462
			50,
463
			50
464
		);
465
466
		try {
467
			$lookup->getReferencedEntityId( $q2013, new PropertyId( 'P31' ), [ new ItemId( 'Q154187' ) ] );
468
		} catch ( ReferencedEntityIdLookupException $exception ) {
469
			$this->assertInstanceOf( EntityLookupException::class, $exception->getPrevious() );
470
471
			return;
472
		}
473
		$this->fail( 'No expection thrown!' );
474
	}
475
476
}
477