Completed
Push — master ( fd45e5...65af16 )
by
unknown
06:32 queued 11s
created

ChangeHandlerTest::getChangeRunCoalescer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Wikibase\Client\Tests\Integration\Changes;
4
5
use ArrayIterator;
6
use InvalidArgumentException;
7
use MediaWiki\MediaWikiServices;
8
use MediaWikiIntegrationTestCase;
9
use Psr\Log\NullLogger;
10
use SiteLookup;
11
use Title;
12
use TitleFactory;
13
use Wikibase\Client\Changes\AffectedPagesFinder;
14
use Wikibase\Client\Changes\ChangeHandler;
15
use Wikibase\Client\Changes\ChangeRunCoalescer;
16
use Wikibase\Client\Changes\PageUpdater;
17
use Wikibase\Client\Usage\EntityUsage;
18
use Wikibase\Client\Usage\PageEntityUsages;
19
use Wikibase\Client\Usage\UsageLookup;
20
use Wikibase\DataModel\Entity\Item;
21
use Wikibase\DataModel\Entity\ItemId;
22
use Wikibase\Lib\Changes\Change;
23
use Wikibase\Lib\Changes\EntityChange;
24
use Wikibase\Lib\Store\SiteLinkLookup;
25
use Wikibase\Lib\Tests\Changes\TestChanges;
26
use Wikibase\Lib\Tests\MockRepository;
27
28
/**
29
 * @covers \Wikibase\Client\Changes\ChangeHandler
30
 *
31
 * @group Wikibase
32
 * @group WikibaseClient
33
 * @group WikibaseChange
34
 *
35
 * @group Database
36
 *
37
 * @license GPL-2.0-or-later
38
 * @author Daniel Kinzler
39
 * @author Jeroen De Dauw < [email protected] >
40
 */
41
class ChangeHandlerTest extends MediaWikiIntegrationTestCase {
42
43
	private function getAffectedPagesFinder( UsageLookup $usageLookup, TitleFactory $titleFactory ) {
44
		// @todo: mock the finder directly
45
		return new AffectedPagesFinder(
46
			$usageLookup,
47
			$titleFactory,
48
			MediaWikiServices::getInstance()->getLinkBatchFactory(),
49
			'enwiki',
50
			null,
51
			false
52
		);
53
	}
54
55
	/**
56
	 * @return ChangeRunCoalescer
57
	 */
58
	private function getChangeRunCoalescer() {
59
		$transformer = $this->getMockBuilder( ChangeRunCoalescer::class )
60
			->disableOriginalConstructor()
61
			->getMock();
62
63
		$transformer->expects( $this->any() )
64
			->method( 'transformChangeList' )
65
			->will( $this->returnArgument( 0 ) );
66
67
		return $transformer;
68
	}
69
70
	private function getChangeHandler(
71
		array $pageNamesPerItemId = [],
72
		PageUpdater $updater = null
73
	) {
74
		$siteLinkLookup = $this->getSiteLinkLookup( $pageNamesPerItemId );
75
		$usageLookup = $this->getUsageLookup( $siteLinkLookup );
76
		$titleFactory = $this->getTitleFactory( $pageNamesPerItemId );
77
		$affectedPagesFinder = $this->getAffectedPagesFinder( $usageLookup, $titleFactory );
78
79
		$handler = new ChangeHandler(
80
			$affectedPagesFinder,
81
			$titleFactory,
82
			$updater ?: new MockPageUpdater(),
83
			$this->getChangeRunCoalescer(),
84
			$this->createMock( SiteLookup::class ),
85
			new NullLogger(),
86
			true
87
		);
88
89
		return $handler;
90
	}
91
92
	/**
93
	 * @param array $pageNamesPerItemId
94
	 *
95
	 * @return SiteLinkLookup
96
	 */
97
	private function getSiteLinkLookup( array $pageNamesPerItemId ) {
98
		$repo = new MockRepository();
0 ignored issues
show
Deprecated Code introduced by
The class Wikibase\Lib\Tests\MockRepository has been deprecated with message: Try to use a simpler fake. The complexity and coupling of this
test double are very high, so it is good to avoid binding to it. Mock repository for use in tests.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
99
100
		// entity 1, revision 11
101
		$entity1 = new Item( new ItemId( 'Q1' ) );
102
		$entity1->setLabel( 'en', 'one' );
103
		$repo->putEntity( $entity1, 11 );
104
105
		// entity 1, revision 12
106
		$entity1->setLabel( 'de', 'eins' );
107
		$repo->putEntity( $entity1, 12 );
108
109
		// entity 1, revision 13
110
		$entity1->setLabel( 'it', 'uno' );
111
		$repo->putEntity( $entity1, 13 );
112
113
		// entity 1, revision 1111
114
		$entity1->setDescription( 'en', 'the first' );
115
		$repo->putEntity( $entity1, 1111 );
116
117
		// entity 2, revision 21
118
		$entity1 = new Item( new ItemId( 'Q2' ) );
119
		$entity1->setLabel( 'en', 'two' );
120
		$repo->putEntity( $entity1, 21 );
121
122
		// entity 2, revision 22
123
		$entity1->setLabel( 'de', 'zwei' );
124
		$repo->putEntity( $entity1, 22 );
125
126
		// entity 2, revision 23
127
		$entity1->setLabel( 'it', 'due' );
128
		$repo->putEntity( $entity1, 23 );
129
130
		// entity 2, revision 1211
131
		$entity1->setDescription( 'en', 'the second' );
132
		$repo->putEntity( $entity1, 1211 );
133
134
		$this->updateMockRepository( $repo, $pageNamesPerItemId );
135
136
		return $repo;
137
	}
138
139
	public function provideHandleChanges() {
140
		$empty = new Item( new ItemId( 'Q55668877' ) );
141
142
		$changeFactory = TestChanges::getEntityChangeFactory();
143
		$itemCreation = $changeFactory->newFromUpdate( EntityChange::ADD, null, $empty );
144
		$itemDeletion = $changeFactory->newFromUpdate( EntityChange::REMOVE, $empty, null );
145
146
		$itemCreation->setField( 'time', '20130101010101' );
147
		$itemDeletion->setField( 'time', '20130102020202' );
148
149
		return [
150
			[],
151
			[ $itemCreation ],
152
			[ $itemDeletion ],
153
			[ $itemCreation, $itemDeletion ],
154
		];
155
	}
156
157
	/**
158
	 * @dataProvider provideHandleChanges
159
	 */
160
	public function testHandleChanges( ...$changes ) {
161
		$spy = (object)[
162
			'handleChangeCallCount' => 0,
163
			'handleChangesCallCount' => 0,
164
		];
165
166
		$testHooks = [
167
			'WikibaseHandleChange' => [ function( Change $change ) use ( $spy ) {
0 ignored issues
show
Unused Code introduced by
The parameter $change is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
168
				$spy->handleChangeCallCount++;
169
				return true;
170
			} ],
171
			'WikibaseHandleChanges' => [ function( array $changes ) use ( $spy ) {
0 ignored issues
show
Unused Code introduced by
The parameter $changes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
172
				$spy->handleChangesCallCount++;
173
				return true;
174
			} ]
175
		];
176
177
		$this->mergeMwGlobalArrayValue( 'wgHooks', $testHooks );
178
179
		$changeHandler = $this->getChangeHandler();
180
		$changeHandler->handleChanges( $changes );
181
182
		$this->assertSame( count( $changes ), $spy->handleChangeCallCount );
183
		$this->assertSame( 1, $spy->handleChangesCallCount );
184
	}
185
186
	/**
187
	 * Returns a map of fake local page IDs to the corresponding local page names.
188
	 * The fake page IDs are the IDs of the items that have a sitelink to the
189
	 * respective page on the local wiki:
190
	 *
191
	 * Example: If Q100 has a link enwiki => 'Emmy',
192
	 * then 100 => 'Emmy' will be in the map returned by this method.
193
	 *
194
	 * @param array[] $pageNamesPerItemId Assoc array mapping entity IDs to lists of sitelinks.
195
	 *
196
	 * @return string[]
197
	 */
198
	private function getFakePageIdMap( array $pageNamesPerItemId ) {
199
		$titlesByPageId = [];
200
		$siteId = 'enwiki';
201
202
		foreach ( $pageNamesPerItemId as $idString => $pageNames ) {
203
			$itemId = new ItemId( $idString );
204
205
			// If $links[0] is set, it's considered a link to the local wiki.
206
			// The index 0 is effectively an alias for $siteId;
207
			if ( isset( $pageNames[0] ) ) {
208
				$pageNames[$siteId] = $pageNames[0];
209
			}
210
211
			if ( isset( $pageNames[$siteId] ) ) {
212
				$pageId = $itemId->getNumericId();
213
				$titlesByPageId[$pageId] = $pageNames[$siteId];
214
			}
215
		}
216
217
		return $titlesByPageId;
218
	}
219
220
	/**
221
	 * Title factory, using spoofed local page ids that correspond to the ids of items linked to
222
	 * the respective page (see getUsageLookup).
223
	 *
224
	 * @param array[] $pageNamesPerItemId Assoc array mapping entity IDs to lists of sitelinks.
225
	 *
226
	 * @return TitleFactory
227
	 */
228
	private function getTitleFactory( array $pageNamesPerItemId ) {
229
		$titlesById = $this->getFakePageIdMap( $pageNamesPerItemId );
230
		$pageIdsByTitle = array_flip( $titlesById );
231
232
		$titleFactory = $this->createMock( TitleFactory::class );
233
234
		$titleFactory->method( 'newFromIDs' )
235
			->willReturnCallback( function ( array $ids ) use ( $titlesById ) {
236
				$titles = [];
237
				foreach ( $ids as $id ) {
238
					if ( isset( $titlesById[$id] ) ) {
239
						$title = Title::newFromText( $titlesById[$id] );
240
						$title->resetArticleID( $id );
241
						$titles[] = $title;
242
					} else {
243
						throw new InvalidArgumentException( 'Unknown ID: ' . $id );
244
					}
245
				}
246
				return $titles;
247
			} );
248
249
		$titleFactory->expects( $this->any() )
250
			->method( 'newFromText' )
251
			->will( $this->returnCallback( function( $text, $defaultNs = \NS_MAIN ) use ( $pageIdsByTitle ) {
252
				$title = Title::newFromText( $text, $defaultNs );
253
254
				if ( !$title ) {
255
					return $title;
256
				}
257
258
				if ( isset( $pageIdsByTitle[$text] ) ) {
259
					$title->resetArticleID( $pageIdsByTitle[$text] );
260
				} else {
261
					throw new InvalidArgumentException( 'Unknown title text: ' . $text );
262
				}
263
264
				return $title;
265
			} ) );
266
267
		return $titleFactory;
268
	}
269
270
	/**
271
	 * Returns a usage lookup based on $siteLinklookup.
272
	 * Local page IDs are spoofed using the numeric item ID as the local page ID.
273
	 *
274
	 * @param SiteLinkLookup $siteLinkLookup
275
	 *
276
	 * @return UsageLookup
277
	 */
278
	private function getUsageLookup( SiteLinkLookup $siteLinkLookup ) {
279
		$usageLookup = $this->createMock( UsageLookup::class );
280
		$usageLookup->expects( $this->any() )
281
			->method( 'getPagesUsing' )
282
			->will( $this->returnCallback(
283
				function( $ids, $aspects ) use ( $siteLinkLookup ) {
284
					$pages = [];
285
286
					foreach ( $ids as $id ) {
287
						if ( !( $id instanceof ItemId ) ) {
288
							continue;
289
						}
290
291
						$links = $siteLinkLookup->getSiteLinksForItem( $id );
292
						foreach ( $links as $link ) {
293
							if ( $link->getSiteId() === 'enwiki' ) {
294
								// we use the numeric item id as the fake page id of the local page!
295
								$usedAspects = array_intersect(
296
									[ EntityUsage::SITELINK_USAGE, EntityUsage::LABEL_USAGE . '.en' ],
297
									$aspects
298
								);
299
								if ( !$usedAspects ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $usedAspects of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
300
									continue;
301
								}
302
								$usages = [];
303
								foreach ( $usedAspects as $aspect ) {
304
									$usages[] = new EntityUsage(
305
										$id,
306
										EntityUsage::splitAspectKey( $aspect )[0],
307
										EntityUsage::splitAspectKey( $aspect )[1]
308
									);
309
								}
310
								$pages[] = new PageEntityUsages(
311
									$id->getNumericId(),
312
									$usages
313
								);
314
							}
315
						}
316
					}
317
318
					return new ArrayIterator( $pages );
319
				} ) );
320
321
		return $usageLookup;
322
	}
323
324
	/**
325
	 * @param MockRepository $mockRepository
326
	 * @param array $pageNamesPerItemId Associative array of item id string => either Item object
327
	 * or array of site id => page name.
328
	 */
329
	private function updateMockRepository( MockRepository $mockRepository, array $pageNamesPerItemId ) {
330
		foreach ( $pageNamesPerItemId as $idString => $pageNames ) {
331
			if ( is_array( $pageNames ) ) {
332
				$item = new Item( new ItemId( $idString ) );
333
334
				foreach ( $pageNames as $siteId => $pageName ) {
335
					if ( !is_string( $siteId ) ) {
336
						$siteId = 'enwiki';
337
					}
338
339
					$item->getSiteLinkList()->addNewSiteLink( $siteId, $pageName );
340
				}
341
			} else {
342
				$item = $pageNames;
343
			}
344
345
			$mockRepository->putEntity( $item );
346
		}
347
	}
348
349
	public function provideHandleChange() {
350
		$changes = TestChanges::getChanges();
351
		$userEmmy2 = Title::newFromText( 'User:Emmy2' )->getPrefixedText();
352
353
		$empty = [
354
			'scheduleRefreshLinks' => [],
355
			'purgeWebCache' => [],
356
			'injectRCRecord' => [],
357
		];
358
359
		$emmy2PurgeParser = [
360
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
361
			'purgeWebCache' => [ 'Emmy2' => true ],
362
			'injectRCRecord' => [ 'Emmy2' => true ],
363
		];
364
365
		$userEmmy2PurgeParser = [
366
			'scheduleRefreshLinks' => [ $userEmmy2 => true ],
367
			'purgeWebCache' => [ $userEmmy2 => true ],
368
			'injectRCRecord' => [ $userEmmy2 => true ],
369
		];
370
371
		$emmyUpdateLinks = [
372
			'scheduleRefreshLinks' => [ 'Emmy' => true ],
373
			'purgeWebCache' => [ 'Emmy' => true ],
374
			'injectRCRecord' => [ 'Emmy' => true ],
375
		];
376
377
		$emmy2UpdateLinks = [
378
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
379
			'purgeWebCache' => [ 'Emmy2' => true ],
380
			'injectRCRecord' => [ 'Emmy2' => true ],
381
		];
382
383
		$emmy2UpdateAll = [
384
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
385
			'purgeWebCache' => [ 'Emmy2' => true ],
386
			'injectRCRecord' => [ 'Emmy2' => true ],
387
		];
388
389
		return [
390
			[ // #0
391
				$changes['property-creation'],
392
				[ 'Q100' => [] ],
393
				$empty
394
			],
395
			[ // #1
396
				$changes['property-deletion'],
397
				[ 'Q100' => [] ],
398
				$empty
399
			],
400
			[ // #2
401
				$changes['property-set-label'],
402
				[ 'Q100' => [] ],
403
				$empty
404
			],
405
406
			[ // #3
407
				$changes['item-creation'],
408
				[ 'Q100' => [] ],
409
				$empty
410
			],
411
			[ // #4
412
				$changes['item-deletion'],
413
				[ 'Q100' => [] ],
414
				$empty
415
			],
416
			[ // #5
417
				$changes['item-deletion-linked'],
418
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
419
				$emmy2UpdateAll
420
			],
421
422
			[ // #6
423
				$changes['set-de-label'],
424
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
425
				$empty, // For the dummy page, only label and sitelink usage is defined.
426
			],
427
			[ // #7
428
				$changes['set-en-label'],
429
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
430
				$emmy2PurgeParser
431
			],
432
			[ // #8
433
				$changes['set-en-label'],
434
				[ 'Q100' => [ 'enwiki' => 'User:Emmy2' ] ], // user namespace
435
				$userEmmy2PurgeParser
436
			],
437
			[ // #9
438
				$changes['set-en-aliases'],
439
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
440
				$empty, // For the dummy page, only label and sitelink usage is defined.
441
			],
442
443
			[ // #10
444
				$changes['add-claim'],
445
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
446
				$empty // statements are ignored
447
			],
448
			[ // #11
449
				$changes['remove-claim'],
450
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
451
				$empty // statements are ignored
452
			],
453
454
			[ // #12
455
				$changes['set-dewiki-sitelink'],
456
				[ 'Q100' => [] ],
457
				$empty // not yet linked
458
			],
459
			[ // #13
460
				$changes['set-enwiki-sitelink'],
461
				[ 'Q100' => [ 'enwiki' => 'Emmy' ] ],
462
				$emmyUpdateLinks
463
			],
464
465
			[ // #14
466
				$changes['change-dewiki-sitelink'],
467
				[ 'Q100' => [ 'enwiki' => 'Emmy' ] ],
468
				$emmyUpdateLinks
469
			],
470
			[ // #15
471
				$changes['change-enwiki-sitelink'],
472
				[ 'Q100' => [ 'enwiki' => 'Emmy' ], 'Q200' => [ 'enwiki' => 'Emmy2' ] ],
473
				[
474
					'scheduleRefreshLinks' => [ 'Emmy' => true, 'Emmy2' => true ],
475
					'purgeWebCache' => [ 'Emmy' => true, 'Emmy2' => true ],
476
					'injectRCRecord' => [ 'Emmy' => true, 'Emmy2' => true ],
477
				]
478
			],
479
			[ // #16
480
				$changes['change-enwiki-sitelink-badges'],
481
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
482
				$emmy2UpdateLinks
483
			],
484
485
			[ // #17
486
				$changes['remove-dewiki-sitelink'],
487
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
488
				$emmy2UpdateLinks
489
			],
490
			[ // #18
491
				$changes['remove-enwiki-sitelink'],
492
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
493
				$emmy2UpdateLinks
494
			],
495
		];
496
	}
497
498
	/**
499
	 * @dataProvider provideHandleChange
500
	 */
501
	public function testHandleChange( EntityChange $change, array $pageNamesPerItemId, array $expected ) {
502
		$updater = new MockPageUpdater();
503
		$handler = $this->getChangeHandler( $pageNamesPerItemId, $updater );
504
505
		$handler->handleChange( $change );
506
		$updates = $updater->getUpdates();
507
508
		$this->assertSameSize( $expected, $updates );
509
510
		foreach ( $expected as $k => $exp ) {
511
			$up = $updates[$k];
512
			$this->assertSame( array_keys( $exp ), array_keys( $up ), $k );
513
		}
514
	}
515
516
	/**
517
	 * @param int|null $id
518
	 * @param string $type
519
	 * @param string $objectId
520
	 * @param array $info
521
	 *
522
	 * @return EntityChange
523
	 */
524
	private function newChange( $id, $type, $objectId, $info = [] ) {
525
		$fields = [
526
			'id' => $id,
527
			'time' => '20121212121212',
528
			'type' => $type,
529
			'objectid' => $objectId,
530
			'info' => $info,
531
		];
532
533
		return new EntityChange( $fields );
534
	}
535
536
	public function provideHandleChange_rootJobParams() {
537
		$ids = [ 18, 19, 17 ]; // note: provide these out of order, to check canonical sorting!
538
		$regularChange = $this->newChange( 17, 'x~y', 'Q100', [] );
539
		$coalescedChange = $this->newChange( 0, 'x~y', 'Q100', [ 'change-ids' => $ids ] );
540
		$strangeChange = $this->newChange( 0, 'x~y', 'Q100', [ 'kittens' => 13 ] );
541
542
		$q100 = new ItemId( 'Q100' );
543
		$usages = [ // note: provide these out of order, to check canonical sorting!
544
			102 => new PageEntityUsages( 102, [ new EntityUsage( $q100, 'X' ) ] ),
545
			101 => new PageEntityUsages( 101, [ new EntityUsage( $q100, 'X' ) ] ),
546
		];
547
548
		$titleBatchHash = 'f0b873699a63c858667e54cd071f7d9209faeda1';
549
		$strangeHash = 'cadbb4899603593164f06a4754f597fdcb2c07b4';
550
551
		$regularRootJobParams = [
552
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
553
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
554
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-id:17" ],
555
		];
556
557
		$coalescedRootJobParams = [
558
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
559
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
560
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-batch:17,18,19" ],
561
		];
562
563
		$strangeRootJobParams = [
564
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
565
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
566
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-hash:$strangeHash" ],
567
		];
568
569
		return [
570
			[ $regularChange, $usages, $regularRootJobParams ],
571
			[ $coalescedChange, $usages, $coalescedRootJobParams ],
572
			[ $strangeChange, $usages, $strangeRootJobParams ],
573
		];
574
	}
575
576
	/**
577
	 * @dataProvider provideHandleChange_rootJobParams
578
	 */
579
	public function testHandleChange_rootJobParams(
580
		EntityChange $change,
581
		array $usages,
582
		array $expectedRootJobParams
583
	) {
584
		$updater = new MockPageUpdater();
585
586
		$affectedPagesFinder = $this->getMockBuilder( AffectedPagesFinder::class )
587
			->disableOriginalConstructor()
588
			->getMock();
589
		$affectedPagesFinder->expects( $this->any() )
590
			->method( 'getAffectedUsagesByPage' )
591
			->will( $this->returnValue( $usages ) );
592
593
		$titleFactory = $this->getMockBuilder( TitleFactory::class )
594
			->disableOriginalConstructor()
595
			->getMock();
596
		$titleFactory->method( 'newFromIDs' )
597
			->willReturnCallback( function ( array $ids ) {
598
				// NOTE: the fake title construction influences the expected hash values
599
				// defined in provideHandleChange_rootJobParams!
600
				$titles = [];
601
				foreach ( $ids as $id ) {
602
					$title = Title::makeTitle( NS_MAIN, 'Page_No_' . $id );
603
					$title->resetArticleID( $id );
604
					$titles[] = $title;
605
				}
606
				return $titles;
607
			} );
608
609
		$handler = new ChangeHandler(
610
			$affectedPagesFinder,
611
			$titleFactory,
612
			$updater,
613
			$this->getChangeRunCoalescer(),
614
			$this->createMock( SiteLookup::class ),
615
			new NullLogger()
616
		);
617
618
		$inputRootJobParams = [ 'rootJobTimestamp' => '20171122040506' ];
619
620
		$handler->handleChange( $change, $inputRootJobParams );
621
		$actualRootJobParams = $updater->getRootJobParams();
622
623
		$this->assertSameSize( $expectedRootJobParams, $actualRootJobParams );
624
625
		foreach ( $expectedRootJobParams as $k => $exp ) {
626
			$act = $actualRootJobParams[$k];
627
			if ( $k !== 'scheduleRefreshLinks' ) {
628
				$this->assertSame( '20171122040506', $act['rootJobTimestamp'], "$k/rootJobTimestamp" );
629
			}
630
			$this->assertSame( $exp['rootJobSignature'], $act['rootJobSignature'], "$k/rootJobSignature" );
631
		}
632
	}
633
634
}
635