Completed
Push — master ( a4baab...0eea42 )
by
unknown
08:53 queued 11s
created

provideUnrelatedAspects()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
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\Usage\EntityUsage;
15
use Wikibase\Client\Usage\ImplicitDescriptionUsageLookup;
16
use Wikibase\Client\Usage\PageEntityUsages;
17
use Wikibase\Client\Usage\UsageLookup;
18
use Wikibase\DataModel\Entity\ItemId;
19
use Wikibase\DataModel\Entity\PropertyId;
20
use Wikibase\Lib\Store\SiteLinkLookup;
21
22
/**
23
 * @covers \Wikibase\Client\Usage\ImplicitDescriptionUsageLookup
24
 *
25
 * @group Wikibase
26
 * @group WikibaseClient
27
 * @group WikibaseUsageTracking
28
 *
29
 * @license GPL-2.0-or-later
30
 */
31
class ImplicitDescriptionUsageLookupTest extends TestCase {
32
33
	private const TEST_CONTENT_LANGUAGE = 'qqx';
34
	private const TEST_PAGE_ID = 123;
35
36
	private function makeLookupForGetUsagesForPage(
37
		?ItemId $itemId,
38
		?EntityUsage $explicitUsage
39
	) {
40
		$globalSiteId = 'wiki';
41
		$contentLanguage = self::TEST_CONTENT_LANGUAGE;
42
		$pageId = self::TEST_PAGE_ID;
43
		$titleText = 'Title text';
44
		$language = $this->createMock( Language::class );
45
		$language->method( 'getCode' )
46
			->willReturn( $contentLanguage );
47
		$title = $this->createMock( Title::class );
48
		$title->method( 'getPrefixedText' )
49
			->willReturn( $titleText );
50
		$title->method( 'getPageLanguage' )
51
			->willReturn( $language );
52
		$titleFactory = $this->createMock( TitleFactory::class );
53
		$titleFactory->method( 'newFromID' )
54
			->with( $pageId )
55
			->willReturn( $title );
56
		$siteLinkLookup = $this->createMock( SiteLinkLookup::class );
57
		$siteLinkLookup->method( 'getItemIdForLink' )
58
			->with( $globalSiteId, $titleText )
59
			->willReturn( $itemId );
60
		$usageLookup = $this->createMock( UsageLookup::class );
61
		$usageLookup->method( 'getUsagesForPage' )
62
			->with( $pageId )
63
			->willReturn( $explicitUsage ? [ $explicitUsage ] : [] );
64
65
		return new ImplicitDescriptionUsageLookup(
66
			$usageLookup,
67
			$titleFactory,
68
			$this->createMock( LinkBatchFactory::class ),
69
			$globalSiteId,
70
			$siteLinkLookup
71
		);
72
	}
73
74
	public function testGetUsagesForPage_addsImplicitUsage() {
75
		$itemId = new ItemId( 'Q123' );
76
		$explicitUsage = new EntityUsage( $itemId, EntityUsage::SITELINK_USAGE );
77
		$usageLookup = $this->makeLookupForGetUsagesForPage(
78
			$itemId,
79
			$explicitUsage
80
		);
81
82
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
83
84
		$this->assertCount( 2, $usages );
85
		$this->assertSame( $explicitUsage, $usages[0] );
86
		$implicitUsage = $usages[1];
87
		$this->assertSame( $itemId, $implicitUsage->getEntityId() );
88
		$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
89
		$this->assertSame( self::TEST_CONTENT_LANGUAGE, $implicitUsage->getModifier() );
90
	}
91
92
	public function testGetUsagesForPage_doesNotDuplicateExplicitUsage() {
93
		$itemId = new ItemId( 'Q123' );
94
		$explicitUsage = new EntityUsage(
95
			$itemId,
96
			EntityUsage::DESCRIPTION_USAGE,
97
			self::TEST_CONTENT_LANGUAGE
98
		);
99
		$usageLookup = $this->makeLookupForGetUsagesForPage(
100
			$itemId,
101
			$explicitUsage
102
		);
103
104
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
105
106
		$this->assertCount( 1, $usages );
107
		$this->assertSame( $explicitUsage, $usages[0] );
108
	}
109
110
	public function testGetUsagesForPage_doesNothingForUnlinkedPage() {
111
		$usageLookup = $this->makeLookupForGetUsagesForPage( null, null );
112
113
		$usages = $usageLookup->getUsagesForPage( self::TEST_PAGE_ID );
114
115
		$this->assertCount( 0, $usages );
116
	}
117
118
	/** @dataProvider provideRelatedAspects */
119
	public function testGetPagesUsing( array $aspects, bool $expect456, bool $expect789 ) {
120
		$globalSiteId = 'wiki';
121
		$entityIds = [
122
			// This entity ID is irrelevant to ImplicitUsageLookup,
123
			// but the inner lookup will return a usage for it.
124
			// We will assert that it’s not thrown away.
125
			new PropertyId( 'P951' ),
126
			// For this entity ID, the inner lookup will already return a usage
127
			// equal to the implicit usage, so there will be nothing to do.
128
			new ItemId( 'Q123' ),
129
			// For this entity ID, the inner lookup will return different usage
130
			// from the implicit usage, so the implicit usage would need to be added.
131
			// However, if $expect456 is false, the implicit usage should not be added
132
			// because it’s not covered by the $aspects.
133
			new ItemId( 'Q456' ),
134
			// For this entity ID, the inner lookup will return no usage at all,
135
			// so the ImplicitUsageLookup would have to add it,
136
			// but only if $expect789 is true (otherwise it’s not covered by the $aspects).
137
			new ItemId( 'Q789' ),
138
			// For the title linked to this entity ID according to the SiteLinkLookup,
139
			// the TitleFactory will return a Title with a 0 article ID,
140
			// indicating that the repo thinks a page is linked to the item,
141
			// but on the client it does not exist (maybe it was just deleted).
142
			new ItemId( 'Q1000' ),
143
		];
144
		$siteLinkLookup = $this->createMock( SiteLinkLookup::class );
145
		$siteLinkLookup->method( 'getLinks' )
146
			->with( [ 123, 456, 789, 1000 ], [ $globalSiteId ] )
147
			->willReturn( [
148
				[ $globalSiteId, 'Page 123', $entityIds[1]->getNumericId() ],
149
				[ $globalSiteId, 'Page 456', $entityIds[2]->getNumericId() ],
150
				[ $globalSiteId, 'Page 789', $entityIds[3]->getNumericId() ],
151
				[ $globalSiteId, 'Deleted page', $entityIds[4]->getNumericId() ],
152
			] );
153
		// The mocked titles will have a page ID matching the page title,
154
		// and page language code lang-x-pageID:
155
		// For example, title Page 123 = page ID 123 = language lang-x-123
156
		$titleFactory = $this->createMock( TitleFactory::class );
157
		$titleFactory->method( 'newFromDBkey' )
158
			->willReturnCallback( function ( $pageName ) {
159
				if ( strpos( $pageName, 'Page ' ) === 0 ) {
160
					$pageId = (int)substr( $pageName, 5 );
161
				} else {
162
					$pageId = 0;
163
				}
164
				$title = $this->createMock( Title::class );
165
				$title->method( 'getArticleID' )
166
					->willReturn( $pageId );
167
				$language = $this->createMock( Language::class );
168
				$language->method( 'getCode' )
169
					->willReturn( "lang-x-$pageId" );
170
				$title->method( 'getPageLanguage' )
171
					->willReturn( $language );
172
				return $title;
173
			} );
174
		$linkBatchFactory = $this->createMock( LinkBatchFactory::class );
175
		$linkBatchFactory->expects( $this->once() )
176
			->method( 'newLinkBatch' )
177
			->willReturnCallback( function ( array $titles ) {
178
				// assert that it’s called with the right (mocked) titles
179
				$pageIds = [];
180
				foreach ( $titles as $title ) {
181
					$pageIds[] = $title->getArticleID();
182
				}
183
				$this->assertSame( [ 123, 456, 789, 0 ], $pageIds );
184
185
				$linkBatch = $this->createMock( LinkBatch::class );
186
				$linkBatch->expects( $this->once() )
187
					->method( 'execute' );
188
				return $linkBatch;
189
			} );
190
		$usageLookup = $this->createMock( UsageLookup::class );
191
		$pageEntityUsages123 = new PageEntityUsages( 123, [
192
			new EntityUsage(
193
				$entityIds[1],
194
				EntityUsage::DESCRIPTION_USAGE,
195
				'lang-x-123'
196
			),
197
		] );
198
		$originalPageEntityUsages123 = clone $pageEntityUsages123;
199
		$pageEntityUsages456 = new PageEntityUsages( 456, [
200
			new EntityUsage( $entityIds[2], EntityUsage::STATEMENT_USAGE ),
201
		] );
202
		$originalPageEntityUsages456 = clone $pageEntityUsages456;
203
		$pageEntityUsages951 = new PageEntityUsages( 951, [
204
			new EntityUsage( $entityIds[0], EntityUsage::OTHER_USAGE ),
205
		] );
206
		$originalPageEntityUsages951 = clone $pageEntityUsages951;
207
		$usageLookup->method( 'getPagesUsing' )
208
			->with( $entityIds, $aspects )
209
			->willReturn( new ArrayIterator( [
210
				$pageEntityUsages123,
211
				$pageEntityUsages456,
212
				$pageEntityUsages951,
213
			] ) );
214
215
		$pageEntityUsages = ( new ImplicitDescriptionUsageLookup(
216
			$usageLookup,
217
			$titleFactory,
218
			$linkBatchFactory,
219
			$globalSiteId,
220
			$siteLinkLookup
221
		) )->getPagesUsing( $entityIds, $aspects );
222
223
		$pageEntityUsages = iterator_to_array( $pageEntityUsages );
224
		$this->assertCount( $expect789 ? 4 : 3, $pageEntityUsages );
225
226
		$this->assertSame( $pageEntityUsages123, $pageEntityUsages[0] );
227
		$this->assertEquals( $originalPageEntityUsages123, $pageEntityUsages123 );
228
229
		$this->assertSame( $pageEntityUsages456, $pageEntityUsages[1] );
230
		$usages456 = array_values( $pageEntityUsages456->getUsages() );
231
		$originalUsages456 = array_values( $originalPageEntityUsages456->getUsages() );
232
		$this->assertCount( $expect456 ? 2 : 1, $usages456 );
233
		// we know the explicit usage comes before the implicit one,
234
		// because PageEntityUsages sorts them and C(laim) < D(escription)
235
		$this->assertSame( $originalUsages456[0], $usages456[0] );
236
		if ( $expect456 ) {
237
			$implicitUsage = $usages456[1];
238
			$this->assertEquals( $entityIds[2], $implicitUsage->getEntityId() );
239
			$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
240
			$this->assertSame( 'lang-x-456', $implicitUsage->getModifier() );
241
		}
242
243
		$this->assertSame( $pageEntityUsages951, $pageEntityUsages[2] );
244
		$this->assertEquals( $originalPageEntityUsages951, $pageEntityUsages951 );
245
246
		if ( $expect789 ) {
247
			/** @var PageEntityUsages $pageEntityUsages789 */
248
			'@phan-var PageEntityUsages $pageEntityUsages789';
249
			$pageEntityUsages789 = $pageEntityUsages[3];
250
			$this->assertSame( 789, $pageEntityUsages789->getPageId() );
251
			$usages789 = array_values( $pageEntityUsages789->getUsages() );
252
			$this->assertCount( 1, $usages789 );
253
			$implicitUsage = $usages789[0];
254
			$this->assertEquals( $entityIds[3], $implicitUsage->getEntityId() );
255
			$this->assertSame( EntityUsage::DESCRIPTION_USAGE, $implicitUsage->getAspect() );
256
			$this->assertSame( 'lang-x-789', $implicitUsage->getModifier() );
257
		}
258
	}
259
260
	public function provideRelatedAspects() {
261
		yield 'description' => [
262
			'aspects' => [ EntityUsage::DESCRIPTION_USAGE ],
263
			'expect456' => true,
264
			'expect789' => true,
265
		];
266
		yield 'description in wiki content language' => [
267
			'aspects' => [ EntityUsage::makeAspectKey(
268
				EntityUsage::DESCRIPTION_USAGE,
269
				self::TEST_CONTENT_LANGUAGE
270
			) ],
271
			'expect456' => false,
272
			'expect789' => false,
273
		];
274
		yield 'description in unrelated language' => [
275
			'aspects' => [ EntityUsage::makeAspectKey(
276
				EntityUsage::DESCRIPTION_USAGE,
277
				'xyz'
278
			) ],
279
			'expect456' => false,
280
			'expect789' => false,
281
		];
282
		yield 'other and description' => [
283
			'aspects' => [
284
				EntityUsage::OTHER_USAGE,
285
				EntityUsage::DESCRIPTION_USAGE,
286
			],
287
			'expect456' => true,
288
			'expect789' => true,
289
		];
290
		yield 'unfiltered' => [
291
			'aspects' => [],
292
			'expect456' => true,
293
			'expect789' => true,
294
		];
295
	}
296
297
	/** @dataProvider provideUnrelatedAspects */
298
	public function testGetPagesUsing_doesNothingForUnrelatedAspects( array $aspects ) {
299
		$entityIds = [ new ItemId( 'Q123' ) ];
300
		$expectedPages = new ArrayIterator( [ 'opaque sentinel' ] );
301
		$usageLookup = $this->createMock( UsageLookup::class );
302
		$usageLookup->method( 'getPagesUsing' )
303
			->with( $entityIds, $aspects )
304
			->willReturn( $expectedPages );
305
306
		$actualPages = ( new ImplicitDescriptionUsageLookup(
307
			$usageLookup,
308
			$this->createMock( TitleFactory::class ),
309
			$this->createMock( LinkBatchFactory::class ),
310
			'wiki',
311
			$this->createMock( SiteLinkLookup::class )
312
		) )->getPagesUsing( $entityIds, $aspects );
313
314
		$this->assertSame(
315
			iterator_to_array( $expectedPages ),
316
			iterator_to_array( $actualPages )
317
		);
318
	}
319
320
	public function provideUnrelatedAspects() {
321
		yield 'statement' => [ [ EntityUsage::STATEMENT_USAGE ] ];
322
		yield 'label' => [ [ EntityUsage::LABEL_USAGE ] ];
323
		yield 'other' => [ [ EntityUsage::OTHER_USAGE ] ];
324
		yield 'sitelink' => [ [ EntityUsage::SITELINK_USAGE ] ];
325
		yield 'title' => [ [ EntityUsage::TITLE_USAGE ] ];
326
		yield 'all' => [ [ EntityUsage::ALL_USAGE ] ];
327
	}
328
329
	public function testGetUnusedEntities() {
330
		$entityIds = [ new ItemId( 'Q123' ) ];
331
		$expectedUsages = [ 'opaque sentinel' ];
332
		$usageLookup = $this->createMock( UsageLookup::class );
333
		$usageLookup->method( 'getUnusedEntities' )
334
			->with( $entityIds )
335
			->willReturn( $expectedUsages );
336
337
		$actualUsages = ( new ImplicitDescriptionUsageLookup(
338
			$usageLookup,
339
			$this->createMock( TitleFactory::class ),
340
			$this->createMock( LinkBatchFactory::class ),
341
			'wiki',
342
			$this->createMock( SiteLinkLookup::class )
343
		) )->getUnusedEntities( $entityIds );
344
345
		$this->assertSame( $expectedUsages, $actualUsages );
346
	}
347
348
}
349