| Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/repo/tests/phpunit/includes/ChangeDispatcherTest.php |
<?php
namespace Wikibase\Test;
use Diff\DiffOp\Diff\Diff;
use Diff\DiffOp\Diff\MapDiff;
use Diff\DiffOp\DiffOpAdd;
use Diff\DiffOp\DiffOpChange;
use Diff\DiffOp\DiffOpRemove;
use Wikibase\Change;
use Wikibase\ChunkAccess;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\EntityChange;
use Wikibase\ItemChange;
use Wikibase\Lib\Reporting\ExceptionHandler;
use Wikibase\Lib\Reporting\MessageReporter;
use Wikibase\Lib\Reporting\NullMessageReporter;
use Wikibase\Repo\ChangeDispatcher;
use Wikibase\Repo\Notifications\ChangeNotificationSender;
use Wikibase\Store\ChangeDispatchCoordinator;
use Wikibase\Store\SubscriptionLookup;
/**
* @covers Wikibase\Repo\ChangeDispatcher
*
* @group Wikibase
* @group WikibaseChange
* @group WikibaseChangeDispatcher
*
* @license GPL-2.0+
* @author Daniel Kinzler
*/
class ChangeDispatcherTest extends \PHPUnit_Framework_TestCase {
/**
* @var array[]
*/
private $subscriptions;
/**
* @var Change[]
*/
private $changes;
/**
* @var string
*/
private $now = '20140303021010';
/**
* @param ChangeDispatchCoordinator $coordinator
* @param array[] &$notifications
*
* @return ChangeDispatcher
*/
private function getChangeDispatcher( ChangeDispatchCoordinator $coordinator, array &$notifications = array() ) {
$dispatcher = new ChangeDispatcher(
$coordinator,
$this->getNotificationSender( $notifications ),
$this->getChunkedChangesAccess(),
$this->getSubscriptionLookup()
);
return $dispatcher;
}
/**
* @param array[] &$notifications An array to receive any notifications,
* each having the form array( $siteID, $changes ).
*
* @return ChangeNotificationSender
*/
private function getNotificationSender( array &$notifications = array() ) {
$sender = $this->getMock( ChangeNotificationSender::class );
$sender->expects( $this->any() )
->method( 'sendNotification' )
->will( $this->returnCallback( function ( $siteID, array $changes ) use ( &$notifications ) {
$notifications[] = array( $siteID, $changes );
} ) );
return $sender;
}
/**
* @return ChunkAccess Guaranteed to only return Change objects from loadChunk.
*/
private function getChunkedChangesAccess() {
$chunkedAccess = $this->getMock( ChunkAccess::class );
$chunkedAccess->expects( $this->any() )
->method( 'loadChunk' )
->will( $this->returnCallback( array( $this, 'getChanges' ) ) );
$chunkedAccess->expects( $this->any() )
->method( 'getRecordId' )
->will( $this->returnCallback( function ( Change $change ) {
return $change->getId();
} ) );
return $chunkedAccess;
}
/**
* @return SubscriptionLookup
*/
private function getSubscriptionLookup() {
$lookup = $this->getMock( SubscriptionLookup::class );
$lookup->expects( $this->any() )
->method( 'getSubscriptions' )
->will( $this->returnCallback( array( $this, 'getSubscriptions' ) ) );
return $lookup;
}
public function getChanges( $fromId, $limit ) {
return array_slice( $this->changes, max( $fromId, 1 ), $limit );
}
public function getSubscriptions( $siteId, array $entityIds ) {
if ( !isset( $this->subscriptions[$siteId] ) ) {
return array();
}
return array_intersect( $this->subscriptions[$siteId], $entityIds );
}
/**
* @return Change[]
*/
private function getAllChanges() {
$changeId = 0;
$addEn = new MapDiff( array( 'enwiki' => new DiffOpAdd( 'Foo' ) ) );
$changeEn = new MapDiff( array( 'enwiki' => new DiffOpChange( 'Foo', 'Bar' ) ) );
$addDe = new MapDiff( array( 'dewiki' => new DiffOpAdd( 'Fuh' ) ) );
$removeDe = new MapDiff( array( 'dewiki' => new DiffOpRemove( 'Fuh' ) ) );
return array(
// index 0 is ignored, or used as the base change.
$this->newChange( 0, new ItemId( 'Q99999' ), sprintf( '201403030100', 0 ) ),
$this->newChange( ++$changeId, new PropertyId( 'P11' ), sprintf( '2014030301%02d', $changeId ) ),
$this->newChange( ++$changeId, new PropertyId( 'P11' ), sprintf( '2014030301%02d', $changeId ) ),
$this->newChange( ++$changeId, new ItemId( 'Q22' ), sprintf( '2014030301%02d', $changeId ) ),
$this->newChange( ++$changeId, new ItemId( 'Q22' ), sprintf( '2014030301%02d', $changeId ) ),
$this->newChange( ++$changeId, new ItemId( 'Q33' ), sprintf( '2014030301%02d', $changeId ), $addEn ),
$this->newChange( ++$changeId, new ItemId( 'Q33' ), sprintf( '2014030301%02d', $changeId ), $changeEn ),
$this->newChange( ++$changeId, new ItemId( 'Q44' ), sprintf( '2014030301%02d', $changeId ), $addDe ),
$this->newChange( ++$changeId, new ItemId( 'Q44' ), sprintf( '2014030301%02d', $changeId ), $removeDe ),
);
}
protected function setUp() {
$this->subscriptions['enwiki'] = array(
new PropertyId( 'P11' ),
new ItemId( 'Q22' ),
// changes to Q33 are relevant because they affect enwiki
);
$this->subscriptions['dewiki'] = array(
new ItemId( 'Q22' ),
// changes to Q22 are relevant because they affect dewiki
);
$this->changes = $this->getAllChanges();
}
/**
* @param int $changeId
* @param EntityId $entityId
* @param string $time
* @param Diff|null $siteLinkDiff
*
* @return Change
*/
private function newChange( $changeId, EntityId $entityId, $time, Diff $siteLinkDiff = null ) {
$changeClass = $entityId->getEntityType() === Item::ENTITY_TYPE
? ItemChange::class
: EntityChange::class;
$change = $this->getMockBuilder( $changeClass )
->disableOriginalConstructor()
->getMock();
$change->expects( $this->never() )
->method( 'getType' );
$change->expects( $this->never() )
->method( 'getUser' );
$change->expects( $this->any() )
->method( 'isEmpty' )
->will( $this->returnValue( false ) );
$change->expects( $this->any() )
->method( 'getTime' )
->will( $this->returnValue( $time ) );
$change->expects( $this->any() )
->method( 'getAge' )
->will( $this->returnValue( (int)wfTimestamp( TS_UNIX, $time ) - (int)wfTimestamp( TS_UNIX, $this->now ) ) );
$change->expects( $this->any() )
->method( 'getId' )
->will( $this->returnValue( $changeId ) );
$change->expects( $this->any() )
->method( 'getObjectId' )
->will( $this->returnValue( $entityId->getSerialization() ) );
$change->expects( $this->any() )
->method( 'getEntityId' )
->will( $this->returnValue( $entityId ) );
$change->expects( $this->any() )
->method( 'getSiteLinkDiff' )
->will( $this->returnValue( $siteLinkDiff ) );
return $change;
}
public function testInitialValues() {
$coordinator = $this->getMock( ChangeDispatchCoordinator::class );
$dispatcher = new ChangeDispatcher(
$coordinator,
$this->getNotificationSender(),
$this->getChunkedChangesAccess(),
$this->getSubscriptionLookup()
);
$this->assertSame( $coordinator, $dispatcher->getDispatchCoordinator() );
$this->assertFalse( $dispatcher->isVerbose() );
$this->assertInstanceOf( MessageReporter::class, $dispatcher->getMessageReporter() );
$this->assertInstanceOf( ExceptionHandler::class, $dispatcher->getExceptionHandler() );
$this->assertSame( 1000, $dispatcher->getBatchSize() );
$this->assertSame( 3, $dispatcher->getBatchChunkFactor() );
$this->assertSame( 15, $dispatcher->getMaxChunks() );
}
public function testSetters() {
$dispatcher = new ChangeDispatcher(
$this->getMock( ChangeDispatchCoordinator::class ),
$this->getNotificationSender(),
$this->getChunkedChangesAccess(),
$this->getSubscriptionLookup()
);
$dispatcher->setVerbose( true );
$reporter = new NullMessageReporter();
$dispatcher->setMessageReporter( $reporter );
$exceptionHandler = $this->getMock( ExceptionHandler::class );
$dispatcher->setExceptionHandler( $exceptionHandler );
$dispatcher->setBatchSize( 1 );
$dispatcher->setBatchChunkFactor( 1 );
$dispatcher->setMaxChunks( 1 );
$this->assertTrue( $dispatcher->isVerbose() );
$this->assertSame( $reporter, $dispatcher->getMessageReporter() );
$this->assertSame( $exceptionHandler, $dispatcher->getExceptionHandler() );
$this->assertSame( 1, $dispatcher->getBatchSize() );
$this->assertSame( 1, $dispatcher->getBatchChunkFactor() );
$this->assertSame( 1, $dispatcher->getMaxChunks() );
}
public function testSelectClient() {
$siteId = 'testwiki';
$expectedClientState = array(
'chd_site' => $siteId,
'chd_db' => $siteId,
'chd_seen' => 0,
'chd_touched' => '20140303000000',
'chd_lock' => null
);
$coordinator = $this->getMock( ChangeDispatchCoordinator::class );
$coordinator->expects( $this->once() )
->method( 'selectClient' )
->will( $this->returnValue( $expectedClientState ) );
$coordinator->expects( $this->never() )
->method( 'initState' );
$dispatcher = $this->getChangeDispatcher( $coordinator );
// This does nothing but call $coordinator->selectClient()
$actualClientState = $dispatcher->selectClient();
$this->assertEquals( $expectedClientState, $actualClientState );
}
public function provideGetPendingChanges() {
$changes = $this->getAllChanges();
return array(
'enwiki: one two three'
=> array( 'enwiki', 0, 3, 1, array( $changes[1], $changes[2], $changes[3] ), 3 ),
'enwiki: four five, chunkFactor=1'
=> array( 'enwiki', 3, 2, 1, array( $changes[4], $changes[5] ), 5 ),
'enwiki: five six, chunkFactor=2, scan to end'
=> array( 'enwiki', 4, 3, 2, array( $changes[5], $changes[6] ), 8 ),
'enwiki: five six, chunkFactor=1, scan to end'
=> array( 'enwiki', 4, 3, 1, array( $changes[5], $changes[6] ), 8 ),
'dewiki: three four seven, chunkFactor=1'
=> array( 'dewiki', 2, 3, 1, array( $changes[3], $changes[4], $changes[7] ), 7 ),
'dewiki: three four seven, chunkFactor=2'
=> array( 'dewiki', 2, 3, 1, array( $changes[3], $changes[4], $changes[7] ), 7 ),
'dewiki: seven eight'
=> array( 'dewiki', 4, 3, 2, array( $changes[7], $changes[8] ), 8 ),
);
}
/**
* @dataProvider provideGetPendingChanges
*/
public function testGetPendingChanges(
$siteId,
$afterId,
$batchSize,
$batchChunkFactor,
array $expectedChanges,
$expectedSeen
) {
$coordinator = $this->getMock( ChangeDispatchCoordinator::class );
$dispatcher = $this->getChangeDispatcher( $coordinator );
$dispatcher->setBatchSize( $batchSize );
$dispatcher->setBatchChunkFactor( $batchChunkFactor );
$pending = $dispatcher->getPendingChanges( $siteId, $afterId );
$this->assertChanges( $expectedChanges, $pending[0] );
$this->assertEquals( $expectedSeen, $pending[1] );
}
public function testGetPendingChanges_maxChunks() {
$chunkAccess = $this->getMock( ChunkAccess::class );
$chunkAccess->expects( $this->exactly( 1 ) )
->method( 'loadChunk' )
->will( $this->returnCallback( array( $this, 'getChanges' ) ) );
$chunkAccess->expects( $this->any() )
->method( 'getRecordId' )
->will( $this->returnCallback( function ( Change $change ) {
return $change->getId();
} ) );
$dispatcher = new ChangeDispatcher(
$this->getMock( ChangeDispatchCoordinator::class ),
$this->getNotificationSender(),
$chunkAccess,
$this->getSubscriptionLookup()
);
// 2 changes are loaded in each chunk
$dispatcher->setBatchSize( 2 );
$dispatcher->setBatchChunkFactor( 1 );
// only process 1 chunk
$dispatcher->setMaxChunks( 1 );
$dispatcher->getPendingChanges( 'dewiki', 0 );
}
public function provideDispatchTo() {
$changes = $this->getAllChanges();
return array(
'enwiki: from the beginning' => array(
3,
array(
'chd_site' => 'enwiki',
'chd_db' => 'enwikidb',
'chd_seen' => 0,
'chd_touched' => '00000000000000',
'chd_lock' => null
),
3,
array(
array( 'enwiki', array( 1, 2, 3 ) )
)
),
'enwiki: scan to end' => array(
3,
array(
'chd_site' => 'enwiki',
'chd_db' => 'enwikidb',
'chd_seen' => 4,
'chd_touched' => $changes[4]->getTime(),
'chd_lock' => null
),
8,
array(
array( 'enwiki', array( 5, 6 ) )
)
),
'dewiki: from the beginning' => array(
3,
array(
'chd_site' => 'dewiki',
'chd_db' => 'dewikidb',
'chd_seen' => 0,
'chd_touched' => '00000000000000',
'chd_lock' => null
),
7,
array(
array( 'dewiki', array( 3, 4, 7 ) )
)
),
'dewiki: offset' => array(
2,
array(
'chd_site' => 'dewiki',
'chd_db' => 'dewikidb',
'chd_seen' => 3,
'chd_touched' => $changes[4]->getTime(),
'chd_lock' => null
),
7,
array(
array( 'dewiki', array( 4, 7 ) )
)
),
);
}
/**
* @dataProvider provideDispatchTo
*/
public function testDispatchTo( $batchSize, array $wikiState, $expectedFinalSeen, array $expectedNotifications ) {
$expectedFinalState = array_merge( $wikiState, array( 'chd_seen' => $expectedFinalSeen ) );
$coordinator = $this->getMock( ChangeDispatchCoordinator::class );
$coordinator->expects( $this->never() )
->method( 'lockClient' );
$coordinator->expects( $this->once() )
->method( 'releaseClient' )
->with( $expectedFinalState );
$notifications = array();
$dispatcher = $this->getChangeDispatcher( $coordinator, $notifications );
$dispatcher->setBatchSize( $batchSize );
$dispatcher->dispatchTo( $wikiState );
$this->assertNotifications( $expectedNotifications, $notifications );
}
private function getChangeIds( array $changes ) {
return array_map( function( Change $change ) {
return $change->getId();
}, $changes );
}
private function assertChanges( array $expected, $actual ) {
$expected = $this->getChangeIds( $expected );
$actual = $this->getChangeIds( $actual );
$this->assertEquals( $expected, $actual );
}
private function assertNotifications( array $expected, array $notifications ) {
foreach ( $notifications as &$n ) {
$n[1] = $this->getChangeIds( $n[1] );
}
$this->assertEquals( $expected, $notifications );
}
}