Completed
Push — master ( 379b27...5b49bb )
by
unknown
12s
created

testGetReferencedEntityIdMaxEntityVisitsExceeded()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 2
eloc 18
nc 2
nop 6
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\Snak\PropertyNoValueSnak;
22
use Wikibase\DataModel\Snak\PropertyValueSnak;
23
use Wikibase\DataModel\Statement\Statement;
24
use Wikibase\DataModel\Statement\StatementList;
25
26
/**
27
 * @covers Wikibase\DataModel\Services\Lookup\EntityRetrievingClosestReferencedEntityIdLookup
28
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
29
 *
30
 * @license GPL-2.0-or-later
31
 * @author Marius Hoch
32
 */
33
class EntityRetrievingClosestReferencedEntityIdLookupTest extends PHPUnit_Framework_TestCase {
34
35
	/**
36
	 * @param EntityLookup $entityLookup
37
	 * @param int|null $expectedNumberOfGetEntityCalls
38
	 * @return EntityLookup
39
	 */
40
	private function restrictEntityLookup( EntityLookup $entityLookup, $expectedNumberOfGetEntityCalls = null ) {
41
		$entityLookupMock = $this->getMock( EntityLookup::class );
42
43
		$entityLookupMock->expects(
44
			$expectedNumberOfGetEntityCalls === null ? $this->any() : $this->exactly( $expectedNumberOfGetEntityCalls )
45
		)
46
			->method( 'getEntity' )
47
			->willReturnCallback( function ( EntityId $entityId ) use ( $entityLookup ) {
48
				return $entityLookup->getEntity( $entityId );
49
			} );
50
51
		return $entityLookupMock;
52
	}
53
54
	/**
55
	 * @param int $expectedPrefetches
56
	 * @return EntityPrefetcher
57
	 */
58
	private function newEntityPrefetcher( $expectedPrefetches ) {
59
		$entityPrefetcher = $this->getMock( EntityPrefetcher::class );
60
		$entityPrefetcher->expects( $this->exactly( $expectedPrefetches ) )
61
			->method( 'prefetch' )
62
			->with( $this->isType( 'array' ) );
63
64
		return $entityPrefetcher;
65
	}
66
67
	/**
68
	 * @param PropertyId $via
69
	 * @param EntityId[] $to
70
	 *
71
	 * @return StatementList
72
	 */
73
	private function newReferencingStatementList( PropertyId $via, array $to ) {
74
		$statementList = new StatementList();
75
76
		foreach ( $to as $toId ) {
77
			$value = new EntityIdValue( $toId );
78
			$mainSnak = new PropertyValueSnak( $via, $value );
79
			$statementList->addStatement( new Statement( $mainSnak ) );
80
		}
81
82
		return $statementList;
83
	}
84
85
	/**
86
	 * @return EntityLookup
87
	 */
88
	private function newReferencingEntityStructure() {
89
		// This returns the following entity structure (all entities linked by P599)
90
		// Q1 -> Q5 -> Q599 -> Q1234
91
		//   \             \
92
		//    \             -- Q12 -> Q404
93
		//     --- Q90 -> Q3
94
		// Note: Q404 doesn't exist
95
96
		$pSubclassOf = new PropertyId( 'P599' );
97
		$q1 = new ItemId( 'Q1' );
98
		$q5 = new ItemId( 'Q5' );
99
		$q599 = new ItemId( 'Q599' );
100
		$q12 = new ItemId( 'Q12' );
101
		$q404 = new ItemId( 'Q404' );
102
		$q1234 = new ItemId( 'Q1234' );
103
		$q90 = new ItemId( 'Q90' );
104
		$q3 = new ItemId( 'Q3' );
105
106
		$lookup = new InMemoryEntityLookup();
107
108
		$lookup->addEntity(
109
			new Item( $q1, null, null, $this->newReferencingStatementList( $pSubclassOf, [ $q5, $q90 ] ) )
110
		);
111
		$lookup->addEntity(
112
			new Item( $q5, null, null, $this->newReferencingStatementList( $pSubclassOf, [ $q599 ] ) )
113
		);
114
		$lookup->addEntity(
115
			new Item( $q599, null, null, $this->newReferencingStatementList( $pSubclassOf, [ $q12, $q1234 ] ) )
116
		);
117
		$lookup->addEntity(
118
			new Item( $q12, null, null, $this->newReferencingStatementList( $pSubclassOf, [ $q404 ] ) )
119
		);
120
		$lookup->addEntity(
121
			new Item( $q90, null, null, $this->newReferencingStatementList( $pSubclassOf, [ $q3 ] ) )
122
		);
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 EntityLookup
131
	 */
132
	private function newCircularReferencingEntityStructure() {
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->newReferencingStatementList( $pSubclassOf, [ $q5, $q90 ] ) )
147
		);
148
		$lookup->addEntity(
149
			new Item( $q5, null, null, $this->newReferencingStatementList( $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->newReferencingEntityStructure();
170
		$circularReferencingEntityStructure = $this->newCircularReferencingEntityStructure();
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->restrictEntityLookup( $entityLookup, $maxEntityVisits * 2 ),
292
			$this->newEntityPrefetcher( $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->restrictEntityLookup( $entityLookup ),
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->restrictEntityLookup( $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->restrictEntityLookup( $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