Completed
Push — master ( 951284...b5c57e )
by
unknown
06:36 queued 11s
created

testGetUsagesForPage_doesNotAddImplicitUsageWithLocalDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace Wikibase\Client\Tests\Unit\Usage;
6
7
use ArrayIterator;
8
use Language;
9
use LinkBatch;
10
use MediaWiki\Cache\LinkBatchFactory;
11
use PHPUnit\Framework\TestCase;
12
use Title;
13
use TitleFactory;
14
use Wikibase\Client\Store\DescriptionLookup;
15
use Wikibase\Client\Usage\EntityUsage;
16
use Wikibase\Client\Usage\ImplicitDescriptionUsageLookup;
17
use Wikibase\Client\Usage\PageEntityUsages;
18
use Wikibase\Client\Usage\UsageLookup;
19
use Wikibase\DataModel\Entity\ItemId;
20
use Wikibase\DataModel\Entity\PropertyId;
21
use Wikibase\Lib\Store\SiteLinkLookup;
22
23
/**
24
 * @covers \Wikibase\Client\Usage\ImplicitDescriptionUsageLookup
25
 *
26
 * @group Wikibase
27
 * @group WikibaseClient
28
 * @group WikibaseUsageTracking
29
 *
30
 * @license GPL-2.0-or-later
31
 */
32
class ImplicitDescriptionUsageLookupTest extends TestCase {
33
34
	private const TEST_CONTENT_LANGUAGE = 'qqx';
35
	private const TEST_PAGE_ID = 123;
36
37
	private function makeLookupForGetUsagesForPage(
38
		?ItemId $itemId,
39
		?EntityUsage $explicitUsage,
40
		bool $allowLocalShortDesc,
41
		?string $localShortDesc
42
	) {
43
		$globalSiteId = 'wiki';
44
		$contentLanguage = self::TEST_CONTENT_LANGUAGE;
45
		$pageId = self::TEST_PAGE_ID;
46
		$titleText = 'Title text';
47
		$language = $this->createMock( Language::class );
48
		$language->method( 'getCode' )
49
			->willReturn( $contentLanguage );
50
		$title = $this->createMock( Title::class );
51
		$title->method( 'getPrefixedText' )
52
			->willReturn( $titleText );
53
		$title->method( 'getPageLanguage' )
54
			->willReturn( $language );
55
		$titleFactory = $this->createMock( TitleFactory::class );
56
		$titleFactory->method( 'newFromID' )
57
			->with( $pageId )
58
			->willReturn( $title );
59
		$descriptionLookup = $this->createMock( DescriptionLookup::class );
60
		$descriptionLookup->method( 'getDescription' )
61
			->with( $title, DescriptionLookup::SOURCE_LOCAL )
62
			->willReturn( $localShortDesc );
63
		$siteLinkLookup = $this->createMock( SiteLinkLookup::class );
64
		$siteLinkLookup->method( 'getItemIdForLink' )
65
			->with( $globalSiteId, $titleText )
66
			->willReturn( $itemId );
67
		$usageLookup = $this->createMock( UsageLookup::class );
68
		$usageLookup->method( 'getUsagesForPage' )
69
			->with( $pageId )
70
			->willReturn(
71
				$explicitUsage
72
					? [ $explicitUsage->getIdentityString() => $explicitUsage ]
73
					: []
74
			);
75
76
		return new ImplicitDescriptionUsageLookup(
77
			$usageLookup,
78
			$titleFactory,
79
			$allowLocalShortDesc,
80
			$descriptionLookup,
81
			$this->createMock( LinkBatchFactory::class ),
82
			$globalSiteId,
83
			$siteLinkLookup
84
		);
85
	}
86
87
	public function testGetUsagesForPage_addsImplicitUsage() {
88
		$itemId = new ItemId( 'Q123' );
89
		$explicitUsage = new EntityUsage( $itemId, EntityUsage::SITELINK_USAGE );
90
		$usageLookup = $this->makeLookupForGetUsagesForPage(
91
			$itemId,
92
			$explicitUsage,
93
			false,
94
			null
95
		);
96
97
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
98
99
		$this->assertCount( 2, $usages );
100
		$this->assertSame( $explicitUsage, reset( $usages ) );
101
		$this->assertSame( $explicitUsage->getIdentityString(), key( $usages ) );
102
		$implicitUsage = next( $usages );
103
		$this->assertSame( $implicitUsage->getIdentityString(), key( $usages ) );
104
		$this->assertSame( $itemId, $implicitUsage->getEntityId() );
105
		$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
106
		$this->assertSame( self::TEST_CONTENT_LANGUAGE, $implicitUsage->getModifier() );
107
	}
108
109
	public function testGetUsagesForPage_doesNotDuplicateExplicitUsage() {
110
		$itemId = new ItemId( 'Q123' );
111
		$explicitUsage = new EntityUsage(
112
			$itemId,
113
			EntityUsage::DESCRIPTION_USAGE,
114
			self::TEST_CONTENT_LANGUAGE
115
		);
116
		$usageLookup = $this->makeLookupForGetUsagesForPage(
117
			$itemId,
118
			$explicitUsage,
119
			false,
120
			null
121
		);
122
123
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
124
125
		$this->assertCount( 1, $usages );
126
		$this->assertEquals( $explicitUsage, reset( $usages ) );
127
		$this->assertSame( $explicitUsage->getIdentityString(), key( $usages ) );
128
	}
129
130
	public function testGetUsagesForPage_ignoresLocalDescriptionIfNotAllowed() {
131
		$itemId = new ItemId( 'Q123' );
132
		$usageLookup = $this->makeLookupForGetUsagesForPage(
133
			$itemId,
134
			null,
135
			false,
136
			'local short description'
137
		);
138
139
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
140
141
		$this->assertCount( 1, $usages );
142
		$implicitUsage = reset( $usages );
143
		$this->assertSame( $implicitUsage->getIdentityString(), key( $usages ) );
144
		$this->assertSame( $itemId, $implicitUsage->getEntityId() );
145
		$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
146
		$this->assertSame( self::TEST_CONTENT_LANGUAGE, $implicitUsage->getModifier() );
147
	}
148
149
	public function testGetUsagesForPage_doesNotAddImplicitUsageWithLocalDescription() {
150
		$itemId = new ItemId( 'Q123' );
151
		$usageLookup = $this->makeLookupForGetUsagesForPage(
152
			$itemId,
153
			null,
154
			true,
155
			'local short description'
156
		);
157
158
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
159
160
		$this->assertEmpty( $usages );
161
	}
162
163
	public function testGetUsagesForPage_doesNotRemoveExplicitUsageWithLocalDescription() {
164
		$itemId = new ItemId( 'Q123' );
165
		$explicitUsage = new EntityUsage(
166
			$itemId,
167
			EntityUsage::DESCRIPTION_USAGE,
168
			self::TEST_CONTENT_LANGUAGE
169
		);
170
		$usageLookup = $this->makeLookupForGetUsagesForPage(
171
			$itemId,
172
			$explicitUsage,
173
			true,
174
			'local short description'
175
		);
176
177
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
178
179
		$this->assertCount( 1, $usages );
180
		$this->assertEquals( $explicitUsage, reset( $usages ) );
181
		$this->assertSame( $explicitUsage->getIdentityString(), key( $usages ) );
182
	}
183
184
	public function testGetUsagesForPage_doesNothingForUnlinkedPage() {
185
		$usageLookup = $this->makeLookupForGetUsagesForPage( null, null, false, null );
186
187
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
188
189
		$this->assertCount( 0, $usages );
190
	}
191
192
	/** @dataProvider provideRelatedAspects */
193
	public function testGetPagesUsing( array $aspects, bool $expect456, bool $expect789 ) {
194
		$globalSiteId = 'wiki';
195
		$entityIds = [
196
			// This entity ID is irrelevant to ImplicitUsageLookup,
197
			// but the inner lookup will return a usage for it.
198
			// We will assert that it’s not thrown away.
199
			new PropertyId( 'P951' ),
200
			// For this entity ID, the inner lookup will already return a usage
201
			// equal to the implicit usage, so there will be nothing to do.
202
			new ItemId( 'Q123' ),
203
			// For this entity ID, the inner lookup will return different usage
204
			// from the implicit usage, so the implicit usage would need to be added.
205
			// However, if $expect456 is false, the implicit usage should not be added
206
			// because it’s not covered by the $aspects.
207
			new ItemId( 'Q456' ),
208
			// For this entity ID, the inner lookup will return no usage at all,
209
			// so the ImplicitUsageLookup would have to add it,
210
			// but only if $expect789 is true (otherwise it’s not covered by the $aspects).
211
			new ItemId( 'Q789' ),
212
			// For the title linked to this entity ID according to the SiteLinkLookup,
213
			// the TitleFactory will return a Title with a 0 article ID,
214
			// indicating that the repo thinks a page is linked to the item,
215
			// but on the client it does not exist (maybe it was just deleted).
216
			new ItemId( 'Q1000' ),
217
		];
218
		$siteLinkLookup = $this->createMock( SiteLinkLookup::class );
219
		$siteLinkLookup->method( 'getLinks' )
220
			->with( [ 123, 456, 789, 1000 ], [ $globalSiteId ] )
221
			->willReturn( [
222
				[ $globalSiteId, 'Page 123', $entityIds[1]->getNumericId() ],
223
				[ $globalSiteId, 'Page 456', $entityIds[2]->getNumericId() ],
224
				[ $globalSiteId, 'Page 789', $entityIds[3]->getNumericId() ],
225
				[ $globalSiteId, 'Deleted page', $entityIds[4]->getNumericId() ],
226
			] );
227
		$titleFactory = $this->mockTitleFactory();
228
		$linkBatchFactory = $this->mockLinkBatchFactory( [ 123, 456, 789, 0 ] );
229
		$usageLookup = $this->createMock( UsageLookup::class );
230
		$pageEntityUsages123 = new PageEntityUsages( 123, [
231
			new EntityUsage(
232
				$entityIds[1],
233
				EntityUsage::DESCRIPTION_USAGE,
234
				'lang-x-123'
235
			),
236
		] );
237
		$originalPageEntityUsages123 = clone $pageEntityUsages123;
238
		$pageEntityUsages456 = new PageEntityUsages( 456, [
239
			new EntityUsage( $entityIds[2], EntityUsage::STATEMENT_USAGE ),
240
		] );
241
		$originalPageEntityUsages456 = clone $pageEntityUsages456;
242
		$pageEntityUsages951 = new PageEntityUsages( 951, [
243
			new EntityUsage( $entityIds[0], EntityUsage::OTHER_USAGE ),
244
		] );
245
		$originalPageEntityUsages951 = clone $pageEntityUsages951;
246
		$usageLookup->method( 'getPagesUsing' )
247
			->with( $entityIds, $aspects )
248
			->willReturn( new ArrayIterator( [
249
				$pageEntityUsages123,
250
				$pageEntityUsages456,
251
				$pageEntityUsages951,
252
			] ) );
253
254
		$pageEntityUsages = ( new ImplicitDescriptionUsageLookup(
255
			$usageLookup,
256
			$titleFactory,
257
			false,
258
			$this->createMock( DescriptionLookup::class ),
259
			$linkBatchFactory,
260
			$globalSiteId,
261
			$siteLinkLookup
262
		) )->getPagesUsing( $entityIds, $aspects );
263
264
		$pageEntityUsages = iterator_to_array( $pageEntityUsages );
265
		$this->assertCount( $expect789 ? 4 : 3, $pageEntityUsages );
266
267
		$this->assertSame( $pageEntityUsages123, $pageEntityUsages[0] );
268
		$this->assertEquals( $originalPageEntityUsages123, $pageEntityUsages123 );
269
270
		$this->assertSame( $pageEntityUsages456, $pageEntityUsages[1] );
271
		$usages456 = array_values( $pageEntityUsages456->getUsages() );
272
		$originalUsages456 = array_values( $originalPageEntityUsages456->getUsages() );
273
		$this->assertCount( $expect456 ? 2 : 1, $usages456 );
274
		// we know the explicit usage comes before the implicit one,
275
		// because PageEntityUsages sorts them and C(laim) < D(escription)
276
		$this->assertSame( $originalUsages456[0], $usages456[0] );
277
		if ( $expect456 ) {
278
			$implicitUsage = $usages456[1];
279
			$this->assertEquals( $entityIds[2], $implicitUsage->getEntityId() );
280
			$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
281
			$this->assertSame( 'lang-x-456', $implicitUsage->getModifier() );
282
		}
283
284
		$this->assertSame( $pageEntityUsages951, $pageEntityUsages[2] );
285
		$this->assertEquals( $originalPageEntityUsages951, $pageEntityUsages951 );
286
287
		if ( $expect789 ) {
288
			/** @var PageEntityUsages $pageEntityUsages789 */
289
			'@phan-var PageEntityUsages $pageEntityUsages789';
290
			$pageEntityUsages789 = $pageEntityUsages[3];
291
			$this->assertSame( 789, $pageEntityUsages789->getPageId() );
292
			$usages789 = array_values( $pageEntityUsages789->getUsages() );
293
			$this->assertCount( 1, $usages789 );
294
			$implicitUsage = $usages789[0];
295
			$this->assertEquals( $entityIds[3], $implicitUsage->getEntityId() );
296
			$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
297
			$this->assertSame( 'lang-x-789', $implicitUsage->getModifier() );
298
		}
299
	}
300
301
	public function provideRelatedAspects() {
302
		yield 'description' => [
303
			'aspects' => [ EntityUsage::DESCRIPTION_USAGE ],
304
			'expect456' => true,
305
			'expect789' => true,
306
		];
307
		yield 'description in wiki content language' => [
308
			'aspects' => [ EntityUsage::makeAspectKey(
309
				EntityUsage::DESCRIPTION_USAGE,
310
				self::TEST_CONTENT_LANGUAGE
311
			) ],
312
			'expect456' => false,
313
			'expect789' => false,
314
		];
315
		yield 'description in unrelated language' => [
316
			'aspects' => [ EntityUsage::makeAspectKey(
317
				EntityUsage::DESCRIPTION_USAGE,
318
				'xyz'
319
			) ],
320
			'expect456' => false,
321
			'expect789' => false,
322
		];
323
		yield 'other and description' => [
324
			'aspects' => [
325
				EntityUsage::OTHER_USAGE,
326
				EntityUsage::DESCRIPTION_USAGE,
327
			],
328
			'expect456' => true,
329
			'expect789' => true,
330
		];
331
		yield 'unfiltered' => [
332
			'aspects' => [],
333
			'expect456' => true,
334
			'expect789' => true,
335
		];
336
	}
337
338
	/** @dataProvider provideUnrelatedAspects */
339
	public function testGetPagesUsing_doesNothingForUnrelatedAspects( array $aspects ) {
340
		$entityIds = [ new ItemId( 'Q123' ) ];
341
		$expectedPages = new ArrayIterator( [ 'opaque sentinel' ] );
342
		$usageLookup = $this->createMock( UsageLookup::class );
343
		$usageLookup->method( 'getPagesUsing' )
344
			->with( $entityIds, $aspects )
345
			->willReturn( $expectedPages );
346
347
		$actualPages = ( new ImplicitDescriptionUsageLookup(
348
			$usageLookup,
349
			$this->createMock( TitleFactory::class ),
350
			false,
351
			$this->createMock( DescriptionLookup::class ),
352
			$this->createMock( LinkBatchFactory::class ),
353
			'wiki',
354
			$this->createMock( SiteLinkLookup::class )
355
		) )->getPagesUsing( $entityIds, $aspects );
356
357
		$this->assertSame(
358
			iterator_to_array( $expectedPages ),
359
			iterator_to_array( $actualPages )
360
		);
361
	}
362
363
	public function provideUnrelatedAspects() {
364
		yield 'statement' => [ [ EntityUsage::STATEMENT_USAGE ] ];
365
		yield 'label' => [ [ EntityUsage::LABEL_USAGE ] ];
366
		yield 'other' => [ [ EntityUsage::OTHER_USAGE ] ];
367
		yield 'sitelink' => [ [ EntityUsage::SITELINK_USAGE ] ];
368
		yield 'title' => [ [ EntityUsage::TITLE_USAGE ] ];
369
		yield 'all' => [ [ EntityUsage::ALL_USAGE ] ];
370
	}
371
372
	public function testGetPagesUsing_filtersByLocalDescription() {
373
		$globalSiteId = 'wiki';
374
		$aspects = [ EntityUsage::DESCRIPTION_USAGE ];
375
		$entityIds = [
376
			// linked to a page without local description (should have implicit usage)
377
			new ItemId( 'Q1234' ),
378
			// linked to a page with local description (should not have implicit usage)
379
			new ItemId( 'Q5678' ),
380
		];
381
		$siteLinkLookup = $this->createMock( SiteLinkLookup::class );
382
		$siteLinkLookup->method( 'getLinks' )
383
			->with( [ 1234, 5678 ], [ $globalSiteId ] )
384
			->willReturn( [
385
				[ $globalSiteId, 'Page 1234', $entityIds[0]->getNumericId() ],
386
				[ $globalSiteId, 'Page 5678', $entityIds[1]->getNumericId() ],
387
			] );
388
		$descriptionLookup = $this->createMock( DescriptionLookup::class );
389
		$descriptionLookup->expects( $this->once() )
390
			->method( 'getDescriptions' )
391
			->with( $this->callback( function ( array $titles ) {
392
				$this->assertCount( 2, $titles );
393
				$this->assertSame( 1234, $titles[0]->getArticleID() );
394
				$this->assertSame( 5678, $titles[1]->getArticleID() );
395
				return true;
396
			} ), DescriptionLookup::SOURCE_LOCAL )
397
			->willReturn( [ 5678 => 'local description' ] );
398
		$titleFactory = $this->mockTitleFactory();
399
		$linkBatchFactory = $this->mockLinkBatchFactory( [ 1234, 5678 ] );
400
		$usageLookup = $this->createMock( UsageLookup::class );
401
		$usageLookup->method( 'getPagesUsing' )
402
			->with( $entityIds, $aspects )
403
			->willReturn( new ArrayIterator( [] ) );
404
405
		$pageEntityUsages = ( new ImplicitDescriptionUsageLookup(
406
			$usageLookup,
407
			$titleFactory,
408
			true,
409
			$descriptionLookup,
410
			$linkBatchFactory,
411
			$globalSiteId,
412
			$siteLinkLookup
413
		) )->getPagesUsing( $entityIds, $aspects );
414
415
		$pageEntityUsages = iterator_to_array( $pageEntityUsages );
416
		$this->assertCount( 1, $pageEntityUsages );
417
		/** @var PageEntityUsages $pageEntityUsages1234 */
418
		'@phan-var PageEntityUsages $pageEntityUsages1234';
419
		$pageEntityUsages1234 = $pageEntityUsages[0];
420
		$this->assertSame( 1234, $pageEntityUsages1234->getPageId() );
421
		$usages1234 = array_values( $pageEntityUsages1234->getUsages() );
422
		$this->assertCount( 1, $usages1234 );
423
		$implicitUsage = $usages1234[0];
424
		$this->assertEquals( $entityIds[0], $implicitUsage->getEntityId() );
425
		$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
426
		$this->assertSame( 'lang-x-1234', $implicitUsage->getModifier() );
427
	}
428
429
	public function testGetUnusedEntities() {
430
		$entityIds = [ new ItemId( 'Q123' ) ];
431
		$expectedUsages = [ 'opaque sentinel' ];
432
		$usageLookup = $this->createMock( UsageLookup::class );
433
		$usageLookup->method( 'getUnusedEntities' )
434
			->with( $entityIds )
435
			->willReturn( $expectedUsages );
436
437
		$actualUsages = ( new ImplicitDescriptionUsageLookup(
438
			$usageLookup,
439
			$this->createMock( TitleFactory::class ),
440
			false,
441
			$this->createMock( DescriptionLookup::class ),
442
			$this->createMock( LinkBatchFactory::class ),
443
			'wiki',
444
			$this->createMock( SiteLinkLookup::class )
445
		) )->getUnusedEntities( $entityIds );
446
447
		$this->assertSame( $expectedUsages, $actualUsages );
448
	}
449
450
	/**
451
	 * Create a mock {@link TitleFactory}, returning titles with a page ID
452
	 * matching the page title, and page language code lang-x-pageID:
453
	 * For example, title Page 123 = page ID 123 = language lang-x-123.
454
	 */
455
	private function mockTitleFactory(): TitleFactory {
456
		$titleFactory = $this->createMock( TitleFactory::class );
457
		$titleFactory->method( 'newFromDBkey' )
458
			->willReturnCallback( function ( $pageName ) {
459
				if ( strpos( $pageName, 'Page ' ) === 0 ) {
460
					$pageId = (int)substr( $pageName, 5 );
461
				} else {
462
					$pageId = 0;
463
				}
464
				$title = $this->createMock( Title::class );
465
				$title->method( 'getArticleID' )
466
					->willReturn( $pageId );
467
				$language = $this->createMock( Language::class );
468
				$language->method( 'getCode' )
469
					->willReturn( "lang-x-$pageId" );
470
				$title->method( 'getPageLanguage' )
471
					->willReturn( $language );
472
				return $title;
473
			} );
474
		return $titleFactory;
475
	}
476
477
	/**
478
	 * Create a mock {@link LinkBatchFactory}, which asserts that
479
	 * {@link LinkBatchFactory::newLinkBatch()} is called with the expected page IDs
480
	 * and then returns a link batch that does nothing on execute.
481
	 * @param int[] $expectedPageIds
482
	 */
483
	private function mockLinkBatchFactory( array $expectedPageIds ): LinkBatchFactory {
484
		$linkBatchFactory = $this->createMock( LinkBatchFactory::class );
485
		$linkBatchFactory->expects( $this->once() )
486
			->method( 'newLinkBatch' )
487
			->willReturnCallback( function ( array $titles ) use ( $expectedPageIds ) {
488
				// assert that it’s called with the right (mocked) titles
489
				$pageIds = [];
490
				foreach ( $titles as $title ) {
491
					$pageIds[] = $title->getArticleID();
492
				}
493
				$this->assertSame( $expectedPageIds, $pageIds );
494
495
				$linkBatch = $this->createMock( LinkBatch::class );
496
				$linkBatch->expects( $this->once() )
497
					->method( 'execute' );
498
				return $linkBatch;
499
			} );
500
		return $linkBatchFactory;
501
	}
502
503
}
504