jenkins-bot has submitted this change and it was merged.
Change subject: Extract change coalescing logic from ChangeHandler
......................................................................
Extract change coalescing logic from ChangeHandler
This introduces the ChangeRunCoalescer class.
Change-Id: Ib0085abc4c1cbf8ca162a031a96008840690907b
---
M client/includes/Changes/ChangeHandler.php
A client/includes/Changes/ChangeListTransformer.php
A client/includes/Changes/ChangeRunCoalescer.php
M client/includes/WikibaseClient.php
M client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
A client/tests/phpunit/includes/Changes/ChangeRunCoalescerTest.php
6 files changed, 793 insertions(+), 938 deletions(-)
Approvals:
Thiemo Mättig (WMDE): Looks good to me, approved
jenkins-bot: Verified
diff --git a/client/includes/Changes/ChangeHandler.php
b/client/includes/Changes/ChangeHandler.php
index 5ff4576..0d6ddbd 100644
--- a/client/includes/Changes/ChangeHandler.php
+++ b/client/includes/Changes/ChangeHandler.php
@@ -6,14 +6,10 @@
use MWException;
use Title;
use Wikibase\Change;
-use Wikibase\Client\Changes\AffectedPagesFinder;
-use Wikibase\Client\Changes\PageUpdater;
use Wikibase\DataModel\Entity\Diff\EntityDiff;
use Wikibase\DataModel\Entity\Diff\ItemDiff;
use Wikibase\EntityChange;
use Wikibase\ItemChange;
-use Wikibase\Lib\Changes\EntityChangeFactory;
-use Wikibase\Lib\Store\EntityRevisionLookup;
use Wikibase\SiteLinkCommentCreator;
/**
@@ -25,9 +21,6 @@
*
* @licence GNU GPL v2+
* @author Daniel Kinzler
- *
- * @fixme: ChangeNotification, ChangeHandler, ClientHooks::onWikibasePollHandle
- * and ClientChangeHandler need to be combined and refactored.
*/
class ChangeHandler {
@@ -63,14 +56,9 @@
private $updater;
/**
- * @var EntityRevisionLookup $entityRevisionLookup
+ * @var ChangeListTransformer
*/
- private $entityRevisionLookup;
-
- /**
- * @var EntityChangeFactory
- */
- private $changeFactory;
+ private $changeListTransformer;
/**
* @var AffectedPagesFinder
@@ -83,18 +71,16 @@
private $localSiteId;
public function __construct(
- EntityChangeFactory $changeFactory,
AffectedPagesFinder $affectedPagesFinder,
PageUpdater $updater,
- EntityRevisionLookup $entityRevisionLookup,
+ ChangeListTransformer $changeListTransformer,
$localSiteId,
$injectRC,
$allowDataTransclusion
) {
- $this->changeFactory = $changeFactory;
+ $this->changeListTransformer = $changeListTransformer;
$this->affectedPagesFinder = $affectedPagesFinder;
$this->updater = $updater;
- $this->entityRevisionLookup = $entityRevisionLookup;
if ( !is_string( $localSiteId ) ) {
throw new InvalidArgumentException( '$localSiteId must
be a string' );
@@ -111,277 +97,6 @@
$this->localSiteId = $localSiteId;
$this->injectRC = (bool)$injectRC;
$this->dataTransclusionAllowed = $allowDataTransclusion;
-
- $this->mirrorUpdater = null;
- }
-
- /**
- * Group changes by the entity they were applied to.
- *
- * @since 0.4
- *
- * @param EntityChange[] $changes
- * @return EntityChange[][] an associative array using entity IDs for
keys. Associated with each
- * entity ID is the list of changes performed on that entity.
- */
- public function groupChangesByEntity( array $changes ) {
- wfProfileIn( __METHOD__ );
- $groups = array();
-
- foreach ( $changes as $change ) {
- $id = $change->getEntityId()->getSerialization();
-
- if ( !isset( $groups[$id] ) ) {
- $groups[$id] = array();
- }
-
- $groups[$id][] = $change;
- }
-
- wfProfileOut( __METHOD__ );
- return $groups;
- }
-
- /**
- * Combines a set of changes into one change. All changes are assume to
have been performed
- * by the same user on the same entity. They are further assumed to be
UPDATE actions
- * and sorted in causal (chronological) order.
- *
- * If $changes is empty, this method returns null. If $changes contains
exactly one change,
- * that change is returned. Otherwise, a combined change is returned.
- *
- * @since 0.4
- *
- * @param EntityChange[] $changes The changes to combine.
- *
- * @throws MWException
- * @return Change a combined change representing the activity from all
the original changes.
- */
- public function mergeChanges( array $changes ) {
- if ( empty( $changes ) ) {
- return null;
- } elseif ( count( $changes ) === 1 ) {
- return reset( $changes );
- }
-
- wfProfileIn( __METHOD__ );
-
- // we now assume that we have a list if EntityChanges,
- // all done by the same user on the same entity.
-
- /* @var EntityChange $last */
- /* @var EntityChange $first */
- $last = end( $changes );
- $first = reset( $changes );
-
- $minor = true;
- $bot = true;
-
- $ids = array();
-
- foreach ( $changes as $change ) {
- $ids[] = $change->getId();
- $meta = $change->getMetadata();
-
- $minor &= isset( $meta['minor'] ) &&
(bool)$meta['minor'];
- $bot &= isset( $meta['bot'] ) && (bool)$meta['bot'];
- }
-
- $lastmeta = $last->getMetadata();
- $firstmeta = $first->getMetadata();
-
- $entityId = $first->getEntityId();
-
- $parentRevId = $firstmeta['parent_id'];
- $latestRevId = $firstmeta['rev_id'];
-
- $entityRev = $this->entityRevisionLookup->getEntityRevision(
$entityId, $latestRevId );
-
- if ( !$entityRev ) {
- throw new MWException( "Failed to load revision
$latestRevId of $entityId" );
- }
-
- $parentRev = $parentRevId ?
$this->entityRevisionLookup->getEntityRevision( $entityId, $parentRevId ) :
null;
-
- //XXX: we could avoid loading the entity data by merging the
diffs programatically
- // instead of re-calculating.
- $change = $this->changeFactory->newFromUpdate(
- $parentRev ? EntityChange::UPDATE : EntityChange::ADD,
- $parentRev === null ? null : $parentRev->getEntity(),
- $entityRev->getEntity()
- );
-
- $change->setFields(
- array(
- 'revision_id' => $last->getField( 'revision_id'
),
- 'user_id' => $last->getField( 'user_id' ),
- 'object_id' => $last->getField( 'object_id' ),
- 'time' => $last->getField( 'time' ),
- )
- );
-
- $change->setMetadata( array_merge(
- $lastmeta,
- array(
- 'parent_id' => $parentRevId,
- 'minor' => $minor,
- 'bot' => $bot,
- )
- //FIXME: size before & size after
- //FIXME: size before & size after
- ) );
-
- $info = $change->hasField( 'info' ) ? $change->getField( 'info'
) : array();
- $info['change-ids'] = $ids;
- $info['changes'] = $changes;
- $change->setField( 'info', $info );
-
- wfProfileOut( __METHOD__ );
- return $change;
- }
-
- /**
- * Coalesce consecutive changes by the same user to the same entity
into one.
- * A run of changes may be broken if the action performed changes (e.g.
deletion
- * instead of update) or if a sitelink pointing to the local wiki was
modified.
- *
- * Some types of actions, like deletion, will break runs.
- * Interleaved changes to different items will break runs.
- *
- * @since 0.4
- *
- * @param EntityChange[] $changes
- * @return EntityChange[] grouped changes
- */
- public function coalesceRuns( array $changes ) {
- wfProfileIn( __METHOD__ );
-
- $coalesced = array();
-
- $currentRun = array();
- $currentUser = null;
- $currentEntity = null;
- $currentAction = null;
- $breakNext = false;
-
- foreach ( $changes as $change ) {
- try {
- $action = $change->getAction();
- $meta = $change->getMetadata();
- $user = $meta['user_text'];
- $entityId =
$change->getEntityId()->__toString();
-
- $break = $breakNext
- || $currentAction !== $action
- || $currentUser !== $user
- || $currentEntity !== $entityId;
-
- $breakNext = false;
-
- if ( !$break && ( $change instanceof ItemChange
) ) {
- $siteLinkDiff =
$change->getSiteLinkDiff();
- if ( isset( $siteLinkDiff[
$this->localSiteId ] ) ) {
- $break = true;
- $breakNext = true;
- };
- }
-
- // FIXME: We should call changeNeedsRendering()
and see if the needs-rendering
- // stays the same, and break the run if
not. This way, uninteresting
- // changes can be sorted out more
cleanly later.
- // FIXME: Perhaps more easily, get rid of them
here and now!
- if ( $break ) {
- if ( !empty( $currentRun ) ) {
- try {
- $coalesced[] =
$this->mergeChanges( $currentRun );
- } catch ( MWException $ex ) {
- // Something went wrong
while trying to merge the changes.
- // Just keep the
original run.
- wfWarn(
$ex->getMessage() );
- $coalesced =
array_merge( $coalesced, $currentRun );
- }
- }
-
- $currentRun = array();
- $currentUser = $user;
- $currentEntity = $entityId;
- $currentAction = $action ===
EntityChange::ADD ? EntityChange::UPDATE : $action;
- }
-
- $currentRun[] = $change;
- // skip any change that failed to process in some way
(bug 49417)
- } catch ( \Exception $e ) {
- wfLogWarning( __METHOD__ . ':' .
$e->getMessage() );
- }
- }
-
- if ( !empty( $currentRun ) ) {
- try {
- $coalesced[] = $this->mergeChanges( $currentRun
);
- } catch ( MWException $ex ) {
- // Something went wrong while trying to merge
the changes.
- // Just keep the original run.
- wfWarn( $ex->getMessage() );
- $coalesced = array_merge( $coalesced,
$currentRun );
- }
- }
-
- wfProfileOut( __METHOD__ );
- return $coalesced;
- }
-
- /**
- * Coalesce changes where possible. This combines any consecutive
changes by the same user
- * to the same entity into one. Interleaved changes to different items
are handled gracefully.
- *
- * @since 0.4
- *
- * @param EntityChange[] $changes
- * @return Change[] grouped changes
- */
- public function coalesceChanges( array $changes ) {
- wfProfileIn( __METHOD__ );
- $coalesced = array();
-
- $changesByEntity = $this->groupChangesByEntity( $changes );
- foreach ( $changesByEntity as $entityChanges ) {
- $entityChanges = $this->coalesceRuns( $entityChanges );
- $coalesced = array_merge( $coalesced, $entityChanges );
- }
-
- usort( $coalesced,
'Wikibase\Client\Changes\ChangeHandler::compareChangesByTimestamp' );
-
- wfDebugLog( __CLASS__, __METHOD__ . ": coalesced "
- . count( $changes ) . " into " . count( $coalesced ) .
" changes" );
-
- wfProfileOut( __METHOD__ );
- return $coalesced;
- }
-
- /**
- * Compares two changes based on their timestamp.
- *
- * @param Change $a
- * @param Change $b
- *
- * @return Mixed
- */
- public static function compareChangesByTimestamp( Change $a, Change $b
) {
- //NOTE: beware https://bugs.php.net/bug.php?id=50688 !
-
- if ( $a->getTime() > $b->getTime() ) {
- return 1;
- } elseif ( $a->getTime() < $b->getTime() ) {
- return -1;
- }
-
- if ( $a->getId() > $b->getId() ) {
- return 1;
- } elseif ( $a->getId() < $b->getId() ) {
- return -1;
- }
-
- return 0;
}
/**
@@ -394,7 +109,7 @@
public function handleChanges( array $changes ) {
wfProfileIn( __METHOD__ );
- $changes = $this->coalesceChanges( $changes );
+ $changes = $this->changeListTransformer->transformChangeList(
$changes );
if ( !wfRunHooks( 'WikibaseHandleChanges', array( $changes ) )
) {
wfProfileOut( __METHOD__ );
@@ -441,11 +156,6 @@
wfDebugLog( __CLASS__, __FUNCTION__ . ": No actions to
take for change #$chid." );
wfProfileOut( __METHOD__ );
return false;
- }
-
- if ( $this->mirrorUpdater !== null && ( $change instanceof
EntityChange ) ) {
- // keep local mirror up to date
- $this->mirrorUpdater->handleChange( $change );
}
$titlesToUpdate = $this->getPagesToUpdate( $change );
diff --git a/client/includes/Changes/ChangeListTransformer.php
b/client/includes/Changes/ChangeListTransformer.php
new file mode 100644
index 0000000..1552a74
--- /dev/null
+++ b/client/includes/Changes/ChangeListTransformer.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Wikibase\Client\Changes;
+
+use Wikibase\Change;
+
+/**
+ * Interface for service objects implementing some transformation on a list of
Change objects.
+ *
+ * @since 0.5
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ *
+ */
+interface ChangeListTransformer {
+
+ /**
+ * Processes the given list of changes, possibly merging, filtering or
otherwise modifying
+ * changes and/or the list of changes.
+ *
+ * @param Change[] $changes
+ *
+ * @return Change[]
+ */
+ public function transformChangeList( array $changes );
+
+}
diff --git a/client/includes/Changes/ChangeRunCoalescer.php
b/client/includes/Changes/ChangeRunCoalescer.php
new file mode 100644
index 0000000..d81c56a
--- /dev/null
+++ b/client/includes/Changes/ChangeRunCoalescer.php
@@ -0,0 +1,319 @@
+<?php
+
+namespace Wikibase\Client\Changes;
+
+use MWException;
+use Wikibase\Change;
+use Wikibase\EntityChange;
+use Wikibase\ItemChange;
+use Wikibase\Lib\Changes\EntityChangeFactory;
+use Wikibase\Lib\Store\EntityRevisionLookup;
+
+/**
+ * ChangeListTransformer implementation that combines runs of changes into a
single change.
+ * A "run" of changes is a sequence of consecutive changes performed by the
same
+ * user, and not interrupted by a "disruptive" change. Changes altering the
association
+ * between pages on the local wiki and items on the repo are considered
disruptive.
+ *
+ * @since 0.5
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ *
+ */
+class ChangeRunCoalescer implements ChangeListTransformer {
+
+ /**
+ * @var EntityRevisionLookup
+ */
+ private $entityRevisionLookup;
+
+ /**
+ * @var EntityChangeFactory
+ */
+ private $changeFactory;
+
+ /**
+ * @var string
+ */
+ private $localSiteId;
+
+ /**
+ * @param EntityRevisionLookup $entityRevisionLookup
+ * @param EntityChangeFactory $changeFactory
+ * @param string $localSiteId
+ */
+ public function __construct(
+ EntityRevisionLookup $entityRevisionLookup,
+ EntityChangeFactory $changeFactory,
+ $localSiteId
+ ) {
+ $this->entityRevisionLookup = $entityRevisionLookup;
+ $this->changeFactory = $changeFactory;
+ $this->localSiteId = $localSiteId;
+ }
+
+ /**
+ * Processes the given list of changes, combining any runs of changes
into a single change.
+ * See the class level documentation for more details on change runs.
+ *
+ * @param Change[] $changes
+ *
+ * @return Change[]
+ */
+ public function transformChangeList( array $changes ) {
+ wfProfileIn( __METHOD__ );
+ $coalesced = array();
+
+ $changesByEntity = $this->groupChangesByEntity( $changes );
+ foreach ( $changesByEntity as $entityChanges ) {
+ $entityChanges = $this->coalesceRuns( $entityChanges );
+ $coalesced = array_merge( $coalesced, $entityChanges );
+ }
+
+ usort( $coalesced,
'Wikibase\Client\Changes\ChangeRunCoalescer::compareChangesByTimestamp' );
+
+ wfDebugLog( __CLASS__, __METHOD__ . ": coalesced "
+ . count( $changes ) . " into " . count( $coalesced ) .
" changes" );
+
+ wfProfileOut( __METHOD__ );
+ return $coalesced;
+ }
+
+
+ /**
+ * Group changes by the entity they were applied to.
+ *
+ * @param EntityChange[] $changes
+ * @return EntityChange[][] an associative array using entity IDs for
keys. Associated with each
+ * entity ID is the list of changes performed on that entity.
+ */
+ private function groupChangesByEntity( array $changes ) {
+ wfProfileIn( __METHOD__ );
+ $groups = array();
+
+ foreach ( $changes as $change ) {
+ $id = $change->getEntityId()->getSerialization();
+
+ if ( !isset( $groups[$id] ) ) {
+ $groups[$id] = array();
+ }
+
+ $groups[$id][] = $change;
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $groups;
+ }
+
+ /**
+ * Combines a set of changes into one change. All changes are assume to
have been performed
+ * by the same user on the same entity. They are further assumed to be
UPDATE actions
+ * and sorted in causal (chronological) order.
+ *
+ * If $changes is empty, this method returns null. If $changes contains
exactly one change,
+ * that change is returned. Otherwise, a combined change is returned.
+ *
+ * @param EntityChange[] $changes The changes to combine.
+ *
+ * @throws MWException
+ * @return Change a combined change representing the activity from all
the original changes.
+ */
+ private function mergeChanges( array $changes ) {
+ if ( empty( $changes ) ) {
+ return null;
+ } elseif ( count( $changes ) === 1 ) {
+ return reset( $changes );
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ // we now assume that we have a list if EntityChanges,
+ // all done by the same user on the same entity.
+
+ /* @var EntityChange $last */
+ /* @var EntityChange $first */
+ $last = end( $changes );
+ $first = reset( $changes );
+
+ $minor = true;
+ $bot = true;
+
+ $ids = array();
+
+ foreach ( $changes as $change ) {
+ $ids[] = $change->getId();
+ $meta = $change->getMetadata();
+
+ $minor &= isset( $meta['minor'] ) &&
(bool)$meta['minor'];
+ $bot &= isset( $meta['bot'] ) && (bool)$meta['bot'];
+ }
+
+ $lastmeta = $last->getMetadata();
+ $firstmeta = $first->getMetadata();
+
+ $entityId = $first->getEntityId();
+
+ $parentRevId = $firstmeta['parent_id'];
+ $latestRevId = $firstmeta['rev_id'];
+
+ $entityRev = $this->entityRevisionLookup->getEntityRevision(
$entityId, $latestRevId );
+
+ if ( !$entityRev ) {
+ throw new MWException( "Failed to load revision
$latestRevId of $entityId" );
+ }
+
+ $parentRev = $parentRevId ?
$this->entityRevisionLookup->getEntityRevision( $entityId, $parentRevId ) :
null;
+
+ //XXX: we could avoid loading the entity data by merging the
diffs programatically
+ // instead of re-calculating.
+ $change = $this->changeFactory->newFromUpdate(
+ $parentRev ? EntityChange::UPDATE : EntityChange::ADD,
+ $parentRev === null ? null : $parentRev->getEntity(),
+ $entityRev->getEntity()
+ );
+
+ $change->setFields(
+ array(
+ 'revision_id' => $last->getField( 'revision_id'
),
+ 'user_id' => $last->getField( 'user_id' ),
+ 'object_id' => $last->getField( 'object_id' ),
+ 'time' => $last->getField( 'time' ),
+ )
+ );
+
+ $change->setMetadata( array_merge(
+ $lastmeta,
+ array(
+ 'parent_id' => $parentRevId,
+ 'minor' => $minor,
+ 'bot' => $bot,
+ )
+ //FIXME: size before & size after
+ //FIXME: size before & size after
+ ) );
+
+ $info = $change->hasField( 'info' ) ? $change->getField( 'info'
) : array();
+ $info['change-ids'] = $ids;
+ $info['changes'] = $changes;
+ $change->setField( 'info', $info );
+
+ wfProfileOut( __METHOD__ );
+ return $change;
+ }
+
+ /**
+ * Coalesce consecutive changes by the same user to the same entity
into one.
+ * A run of changes may be broken if the action performed changes (e.g.
deletion
+ * instead of update) or if a sitelink pointing to the local wiki was
modified.
+ *
+ * Some types of actions, like deletion, will break runs.
+ * Interleaved changes to different items will break runs.
+ *
+ * @param EntityChange[] $changes
+ * @return EntityChange[] grouped changes
+ */
+ private function coalesceRuns( array $changes ) {
+ wfProfileIn( __METHOD__ );
+
+ $coalesced = array();
+
+ $currentRun = array();
+ $currentUser = null;
+ $currentEntity = null;
+ $currentAction = null;
+ $breakNext = false;
+
+ foreach ( $changes as $change ) {
+ try {
+ $action = $change->getAction();
+ $meta = $change->getMetadata();
+ $user = $meta['user_text'];
+ $entityId =
$change->getEntityId()->__toString();
+
+ $break = $breakNext
+ || $currentAction !== $action
+ || $currentUser !== $user
+ || $currentEntity !== $entityId;
+
+ $breakNext = false;
+
+ if ( !$break && ( $change instanceof ItemChange
) ) {
+ $siteLinkDiff =
$change->getSiteLinkDiff();
+ if ( isset( $siteLinkDiff[
$this->localSiteId ] ) ) {
+ $break = true;
+ $breakNext = true;
+ };
+ }
+
+ // FIXME: We should call changeNeedsRendering()
and see if the needs-rendering
+ // stays the same, and break the run if
not. This way, uninteresting
+ // changes can be sorted out more
cleanly later.
+ // FIXME: Perhaps more easily, get rid of them
here and now!
+ if ( $break ) {
+ if ( !empty( $currentRun ) ) {
+ try {
+ $coalesced[] =
$this->mergeChanges( $currentRun );
+ } catch ( MWException $ex ) {
+ // Something went wrong
while trying to merge the changes.
+ // Just keep the
original run.
+ wfWarn(
$ex->getMessage() );
+ $coalesced =
array_merge( $coalesced, $currentRun );
+ }
+ }
+
+ $currentRun = array();
+ $currentUser = $user;
+ $currentEntity = $entityId;
+ $currentAction = $action ===
EntityChange::ADD ? EntityChange::UPDATE : $action;
+ }
+
+ $currentRun[] = $change;
+ // skip any change that failed to process in
some way (bug 49417)
+ } catch ( \Exception $e ) {
+ wfLogWarning( __METHOD__ . ':' .
$e->getMessage() );
+ }
+ }
+
+ if ( !empty( $currentRun ) ) {
+ try {
+ $coalesced[] = $this->mergeChanges( $currentRun
);
+ } catch ( MWException $ex ) {
+ // Something went wrong while trying to merge
the changes.
+ // Just keep the original run.
+ wfWarn( $ex->getMessage() );
+ $coalesced = array_merge( $coalesced,
$currentRun );
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $coalesced;
+ }
+
+ /**
+ * Compares two changes based on their timestamp.
+ *
+ * @param Change $a
+ * @param Change $b
+ *
+ * @return int
+ */
+ public static function compareChangesByTimestamp( Change $a, Change $b
) {
+ //NOTE: beware https://bugs.php.net/bug.php?id=50688 !
+
+ if ( $a->getTime() > $b->getTime() ) {
+ return 1;
+ } elseif ( $a->getTime() < $b->getTime() ) {
+ return -1;
+ }
+
+ if ( $a->getId() > $b->getId() ) {
+ return 1;
+ } elseif ( $a->getId() < $b->getId() ) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+}
\ No newline at end of file
diff --git a/client/includes/WikibaseClient.php
b/client/includes/WikibaseClient.php
index 7315fa5..2f8f08f 100644
--- a/client/includes/WikibaseClient.php
+++ b/client/includes/WikibaseClient.php
@@ -16,6 +16,7 @@
use ValueFormatters\FormatterOptions;
use Wikibase\Client\Changes\ChangeHandler;
use Wikibase\Client\Changes\AffectedPagesFinder;
+use Wikibase\Client\Changes\ChangeRunCoalescer;
use Wikibase\Client\Hooks\LanguageLinkBadgeDisplay;
use Wikibase\Client\Hooks\OtherProjectsSidebarGenerator;
use Wikibase\Client\Hooks\ParserFunctionRegistrant;
@@ -740,12 +741,17 @@
* @return ChangeHandler
*/
public function getChangeHandler() {
+ $siteId = $this->getSite()->getGlobalId();
+
return new ChangeHandler(
- $this->getEntityChangeFactory(),
$this->getAffectedPagesFinder(),
new WikiPageUpdater(),
- $this->getStore()->getEntityRevisionLookup(),
- $this->getSite()->getGlobalId(),
+ new ChangeRunCoalescer(
+ $this->getStore()->getEntityRevisionLookup(),
+ $this->getEntityChangeFactory(),
+ $siteId
+ ),
+ $siteId,
$this->getSettings()->getSetting( 'injectRecentChanges'
),
$this->getSettings()->getSetting(
'allowDataTransclusion' )
);
diff --git a/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
b/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
index 3e63369..814d8b6 100644
--- a/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
+++ b/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
@@ -5,16 +5,14 @@
use ArrayIterator;
use Diff\Differ\MapDiffer;
use Site;
-use SiteList;
use Title;
use Wikibase\Change;
-use Wikibase\Client\Changes\ChangeHandler;
-use Wikibase\ChangesTable;
use Wikibase\Client\Changes\AffectedPagesFinder;
+use Wikibase\Client\Changes\ChangeHandler;
+use Wikibase\Client\Changes\PageUpdater;
use Wikibase\Client\Store\TitleFactory;
use Wikibase\Client\Usage\UsageLookup;
use Wikibase\Client\WikibaseClient;
-use Wikibase\DataModel\Entity\Diff\EntityDiff;
use Wikibase\DataModel\Entity\Entity;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
@@ -23,8 +21,6 @@
use Wikibase\Lib\Store\SiteLinkLookup;
use Wikibase\Lib\Store\StorageException;
use Wikibase\NamespaceChecker;
-use Wikibase\Client\Changes\PageUpdater;
-use Wikibase\Client\Tests\Changes\MockPageUpdater;
use Wikibase\Test\MockRepository;
use Wikibase\Test\TestChanges;
@@ -62,11 +58,11 @@
$usageLookup = $this->getUsageLookup( $repo );
$titleFactory = $this->getTitleFactory( $entities );
- $changeFactory = TestChanges::getEntityChangeFactory();
+ $transformer = $this->getMock(
'Wikibase\Client\Changes\ChangeListTransformer' );
- if ( !$updater ) {
- $updater = new MockPageUpdater();
- }
+ $transformer->expects( $this->any() )
+ ->method( 'transformChangeList' )
+ ->will( $this->returnArgument( 0 ) );
$namespaceChecker = new NamespaceChecker( array(), array(
NS_MAIN ) );
@@ -80,10 +76,9 @@
);
$handler = new ChangeHandler(
- $changeFactory,
$affectedPagesFinder,
- $updater,
- $repo,
+ $updater ? : new MockPageUpdater(),
+ $transformer,
'enwiki',
true,
true
@@ -136,606 +131,7 @@
return $repo;
}
- /**
- * @param array $values
- * @param EntityDiff|null $diff
- *
- * @return EntityChange
- */
- public static function makeChange( array $values, EntityDiff $diff =
null ) {
- if ( !isset( $values['info'] ) ) {
- $values['info'] = array();
- }
-
- if ( !isset( $values['info']['metadata'] ) ) {
- $values['info']['metadata'] = array();
- }
-
- if ( !isset( $values['info']['metadata']['rev_id'] ) && isset(
$values['revision_id'] ) ) {
- $values['info']['metadata']['rev_id'] = $values[
'revision_id' ];
- }
-
- if ( !isset( $values['info']['metadata']['user_text'] ) &&
isset( $values['user_id'] ) ) {
- $values['info']['metadata']['user_text'] = "User" .
$values['user_id'];
- }
-
- if ( !isset( $values['info']['metadata']['parent_id'] ) &&
isset( $values['parent_id'] ) ) {
- $values['info']['metadata']['parent_id'] =
$values['parent_id'];
- }
-
- if ( !isset( $values['info']['metadata']['parent_id'] ) ) {
- $values['info']['metadata']['parent_id'] = 0;
- }
-
- $values['info'] = serialize( $values['info'] );
-
- /* @var EntityChange $change */
- $table = ChangesTable::singleton();
- $change = $table->newRow( $values, true );
-
- if ( $diff ) {
- $change->setDiff( $diff );
- }
-
- return $change;
- }
-
- public static function makeDiff( $type, $before, $after ) {
- $differ = new MapDiffer( true );
-
- $diffOps = $differ->doDiff( $before, $after );
- $diff = EntityDiff::newForType( $type, $diffOps );
-
- return $diff;
- }
-
- public static function provideGroupChangesByEntity() {
- $entity1 = 'Q1';
- $entity2 = 'Q2';
-
- $changes = array( // $changes
-
- self::makeChange( array(
- 'id' => 1,
- 'type' => 'wikibase-item~update',
- 'time' => '20130101010101',
- 'object_id' => $entity1,
- 'revision_id' => 11,
- 'user_id' => 1,
- )),
-
- self::makeChange( array(
- 'id' => 2,
- 'type' => 'wikibase-item~update',
- 'time' => '20130102020202',
- 'object_id' => $entity2,
- 'revision_id' => 21,
- 'user_id' => 1,
- )),
-
- self::makeChange( array(
- 'id' => 1,
- 'type' => 'wikibase-item~update',
- 'time' => '20130103030303',
- 'object_id' => $entity1,
- 'revision_id' => 12,
- 'user_id' => 2,
- )),
- );
-
- return array(
- array( // #0: empty
- array(), // $changes
- array(), // $expectedGroups
- ),
-
- array( // #1: two groups
- $changes, // $changes
- array( // $expectedGroups
- $entity1 => array( $changes[0],
$changes[2] ),
- $entity2 => array( $changes[1] ),
- )
- )
- );
- }
-
- /**
- * @dataProvider provideGroupChangesByEntity
- */
- public function testGroupChangesByEntity( $changes, $expectedGroups ) {
- $handler = $this->newChangeHandler();
- $groups = $handler->groupChangesByEntity( $changes );
-
- $this->assertEquals( count( $expectedGroups ), count( $groups
), "number of groups" );
- $this->assertArrayEquals( array_keys( $expectedGroups ),
array_keys( $groups ), false, false );
-
- foreach ( $groups as $entityId => $group ) {
- $expected = $expectedGroups[$entityId];
- $this->assertArrayEquals( $expected, $group, true,
false );
- }
- }
-
- protected static function getChangeFields( EntityChange $change ) {
- $fields = $change->getFields();
- unset( $fields['id'] );
-
- return $fields;
- }
-
- protected function assertChangeEquals( Change $expected, Change
$actual, $message = null ) {
- if ( $message ) {
- $message .= ': ';
- } else {
- $message = 'change.';
- }
-
- $this->assertEquals( get_class( $expected ), get_class( $actual
), $message . "class" );
-
- $this->assertEquals( $expected->getObjectId(),
$actual->getObjectId(), $message . "ObjectId" );
- $this->assertEquals( $expected->getTime(), $actual->getTime(),
$message . "Time" );
- $this->assertEquals( $expected->getType(), $actual->getType(),
$message . "Type" );
- $this->assertEquals( $expected->getUser(), $actual->getUser(),
$message . "User" );
-
- if ( $expected instanceof EntityChange && $actual instanceof
EntityChange ) {
- $this->assertEquals( $expected->getAction(),
$actual->getAction(), $message . "Action" );
- $this->assertArrayEquals( $expected->getMetadata(),
$actual->getMetadata(), false, true );
- }
- }
-
- public static function provideMergeChanges() {
- $entity1 = 'q1';
-
- $change1 = self::makeChange( array(
- 'id' => 1,
- 'type' => 'wikibase-item~add',
- 'time' => '20130101010101',
- 'object_id' => $entity1,
- 'revision_id' => 11,
- 'user_id' => 1,
- 'info' => array(
- 'metadata' => array (
- 'user_text' => 'User1',
- 'bot' => 0,
- 'page_id' => 1,
- 'rev_id' => 11,
- 'parent_id' => 0,
- 'comment' => 'wikibase-comment-add',
- ),
- )
- ));
-
- $change2 = self::makeChange( array(
- 'id' => 2,
- 'type' => 'wikibase-item~update',
- 'time' => '20130102020202',
- 'object_id' => $entity1,
- 'revision_id' => 12,
- 'user_id' => 1,
- 'info' => array(
- 'metadata' => array (
- 'user_text' => 'User1',
- 'bot' => 0,
- 'page_id' => 1,
- 'rev_id' => 12,
- 'parent_id' => 11,
- 'comment' => 'wikibase-comment-add',
- ),
- )
- ));
-
- $change3 = self::makeChange( array(
- 'id' => 1,
- 'type' => 'wikibase-item~update',
- 'time' => '20130103030303',
- 'object_id' => $entity1,
- 'revision_id' => 13,
- 'user_id' => 1,
- 'info' => array(
- 'metadata' => array (
- 'user_text' => 'User1',
- 'bot' => 0,
- 'page_id' => 1,
- 'rev_id' => 13,
- 'parent_id' => 12,
- 'comment' => 'wikibase-comment-add',
- ),
- )
- ));
-
- $change0 = self::makeChange( array(
- 'id' => 1,
- 'type' => 'wikibase-item~add',
- 'time' => '20130101010101',
- 'object_id' => $entity1,
- 'revision_id' => 0xdeadbeef, // invalid
- 'user_id' => 1,
- 'info' => array(
- 'metadata' => array (
- 'user_text' => 'User1',
- 'bot' => 0,
- 'page_id' => 1,
- 'rev_id' => 0xdeadbeef, // invalid
- 'parent_id' => 0,
- 'comment' => 'wikibase-comment-add',
- ),
- )
- ));
-
- $changeMerged = self::makeChange( array(
- 'id' => null,
- 'type' => 'wikibase-item~add', // because the first
change has no parent
- 'time' => '20130103030303', // last change's timestamp
- 'object_id' => $entity1,
- 'revision_id' => 13, // last changes rev id
- 'user_id' => 1,
- 'info' => array(
- 'metadata' => array (
- 'user_text' => 'User1',
- 'bot' => 0,
- 'page_id' => 1,
- 'rev_id' => 13, // rev id from last
change
- 'parent_id' => 0, // parent rev from
first change
- 'comment' => 'wikibase-comment-add',
- ),
- )
- ));
-
- return array(
- array( // #0: empty
- array(), // $changes
- null, // $expected
- ),
-
- array( // #1: single
- array( $change1 ), // $changes
- $change1, // $expected
- ),
-
- array( // #2: merged
- array( $change1, $change2, $change3 ), //
$changes
- $changeMerged, // $expected
- ),
-
- array( // #3: bad
- array( $change0, $change2, $change3 ), //
$changes
- null, // $expected
- 'MWException', // $error
- ),
- );
- }
-
- /**
- * @dataProvider provideMergeChanges
- */
- public function testMergeChanges( $changes, $expected, $error = null ) {
- try {
- $handler = $this->newChangeHandler();
- $merged = $handler->mergeChanges( $changes );
-
- if ( $error ) {
- $this->fail( "error expected: $error" );
- }
-
- if ( !$expected ) {
- $this->assertEquals( $expected, $merged );
- } else {
- $this->assertChangeEquals( $expected, $merged );
- }
- } catch ( \MWException $ex ) {
- if ( !$error ) {
- throw $ex;
- }
-
- $this->assertInstanceOf( $error, $ex, "expected error"
);
- }
- }
-
- /**
- * @todo: move to TestChanges, unify with TestChanges::getChanges()
- */
- public static function makeTestChanges( $userId, $numericId ) {
- $prefixedId = 'Q' . $numericId;
-
- $offset = 100 * $numericId + 1000 * $userId;
-
- // create with a label and site link set
- $create = self::makeChange( array(
- 'id' => $offset + 1,
- 'type' => 'wikibase-item~add',
- 'time' => '20130101010101',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 11,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'label' => array( 'en' => 'Test' ),
- 'links' => array( 'enwiki' => 'Test' ), // old
style sitelink representation
- )
- ) );
-
- // set a label
- $update = self::makeChange( array(
- 'id' => $offset + 23,
- 'type' => 'wikibase-item~update',
- 'time' => '20130102020202',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 12,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'label' => array( 'de' => 'Test' ),
- )
- ) );
-
- // merged change consisting of $create and $update
- $create_update = self::makeChange( array(
- 'id' => null,
- 'type' => $create->getField('type'), // because the
first change has no parent
- 'time' => $update->getField('time'), // last change's
timestamp
- 'object_id' => $update->getField('object_id'),
- 'revision_id' => $update->getField('revision_id'), //
last changes rev id
- 'user_id' => $update->getField('user_id'),
- 'info' => array(
- 'metadata' => array(
- 'bot' => 0,
- 'comment' => 'wikibase-comment-add' //
this assumes a specific 'type'
- )
- )
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
- 'links' => array( 'enwiki' => 'Test' ), // old
style sitelink representation
- )
- ) );
-
- // change link to other wiki
- $updateXLink = self::makeChange( array(
- 'id' => $offset + 14,
- 'type' => 'wikibase-item~update',
- 'time' => '20130101020304',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 13,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'links' => array( 'dewiki' => array( 'name' =>
'Testen', 'badges' => array() ) ),
- )
- ) );
-
- // merged change consisting of $create, $update and $updateXLink
- $create_update_link = self::makeChange( array(
- 'id' => null,
- 'type' => $create->getField('type'), // because the
first change has no parent
- 'time' => $updateXLink->getField('time'), // last
change's timestamp
- 'object_id' => $updateXLink->getField('object_id'),
- 'revision_id' => $updateXLink->getField('revision_id'),
// last changes rev id
- 'user_id' => $updateXLink->getField('user_id'),
- 'info' => array(
- 'metadata' => array(
- 'bot' => 0,
- 'comment' => 'wikibase-comment-add' //
this assumes a specific 'type'
- )
- )
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
- 'links' => array(
- 'enwiki' => array( 'name' => 'Test' ),
// incomplete new style sitelink representation
- 'dewiki' => array( 'name' => 'Test' ),
// incomplete new style sitelink representation
- ),
- )
- ) );
-
- // some other user changed a label
- $updateX = self::makeChange( array(
- 'id' => $offset + 12,
- 'type' => 'wikibase-item~update',
- 'time' => '20130103030303',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 14,
- 'user_id' => $userId + 17,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(),
- array(
- 'label' => array( 'fr' => array( 'name' =>
'Test', 'badges' => array() ) ),
- )
- ) );
-
- // change link to local wiki
- $updateLink = self::makeChange( array(
- 'id' => $offset + 13,
- 'type' => 'wikibase-item~update',
- 'time' => '20130102030405',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 17,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(
- 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q555' ) ) ),
- ),
- array(
- 'links' => array( 'enwiki' => array( 'name' =>
'Spam', 'badges' => array( 'Q12345' ) ) ),
- )
- ) );
-
- // change only badges in link to local wiki
- $updateLinkBadges = self::makeChange( array(
- 'id' => $offset + 14,
- 'type' => 'wikibase-item~update',
- 'time' => '20130102030405',
- 'object_id' => $prefixedId,
- 'revision_id' => $offset + 18,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(
- 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q555' ) ) ),
- ),
- array(
- 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q12345' ) ) ),
- )
- ) );
-
- // item deleted
- $delete = self::makeChange( array(
- 'id' => $offset + 35,
- 'type' => 'wikibase-item~remove',
- 'time' => '20130105050505',
- 'object_id' => $prefixedId,
- 'revision_id' => 0,
- 'user_id' => $userId,
- ), self::makeDiff( Item::ENTITY_TYPE,
- array(
- 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
- 'links' => array( 'enwiki' => 'Test', 'dewiki'
=> 'Test' ),
- ),
- array()
- ) );
-
- return array(
- 'create' => $create, // create item
- 'update' => $update, // update item
- 'create+update' => $create_update, // merged create and
update
- 'update/other' => $updateX, // update by another
user
- 'update-link/local' => $updateLink, // change the link
to this client wiki
- 'update-link/local/basges' => $updateLinkBadges, //
change the link to this client wiki
- 'update-link/other' => $updateXLink, // change the link
to some other client wiki
- 'create+update+update-link/other' =>
$create_update_link, // merged create and update and update link to other wiki
- 'delete' => $delete, // delete item
- );
- }
-
- public static function provideCoalesceRuns() {
- $changes = self::makeTestChanges( 1, 1 );
-
- $create = $changes['create']; // create item
- $update = $changes['update']; // update item
- $create_update = $changes['create+update']; // merged create
and update
- $updateX = $changes['update/other']; // update by another user
- $updateLink = $changes['update-link/local']; // change the link
to this client wiki
- $updateXLink = $changes['update-link/other']; // change the
link to some other client wiki
- $create_update_link =
$changes['create+update+update-link/other']; // merged create and update and
update link to other wiki
- $delete = $changes['delete']; // delete item
-
- return array(
- array( // #0: empty
- array(), // $changes
- array(), // $expected
- ),
-
- array( // #1: single
- array( $create ), // $changes
- array( $create ), // $expected
- ),
-
- array( // #2: create and update
- array( $create, $update ), // $changes
- array( $create_update ), // $expected
- ),
-
- array( // #3: user change
- array( $create, $updateX, $update ), // $changes
- array( $create, $updateX, $update ), //
$expected
- ),
-
- array( // #4: action change
- array( $create, $update, $delete ), // $changes
- array( $create_update, $delete ), // $expected
- ),
-
- array( // #5: relevant link manipulation
- array( $create, $updateLink, $update ), //
$changes
- array( $create, $updateLink, $update ), //
$expected
- ),
-
- array( // #6: irrelevant link manipulation
- array( $create, $update, $updateXLink ), //
$changes
- array( $create_update_link ), // $expected
- ),
- );
- }
-
- /**
- * @dataProvider provideCoalesceRuns
- */
- public function testCoalesceRuns( $changes, $expected ) {
- $handler = $this->newChangeHandler();
- $coalesced = $handler->coalesceRuns( $changes );
-
- $this->assertEquals( count( $expected ), count( $coalesced ),
"number of changes" );
-
- $i = 0;
- while ( next( $coalesced ) && next( $expected ) ) {
- $this->assertChangeEquals( current( $expected ),
current( $coalesced ), "expected[" . $i++ . "]" );
- }
- }
-
- public static function provideCoalesceChanges() {
- $changes11 = self::makeTestChanges( 1, 1 );
-
- $create11 = $changes11['create']; // create item
- $update11 = $changes11['update']; // update item
- $updateXLink11 = $changes11['update-link/other']; // change the
link to some other client wiki
- $create_update_link11 =
$changes11['create+update+update-link/other']; // merged create and update and
update link to other wiki
- $delete11 = $changes11['delete']; // delete item
-
- $changes12 = self::makeTestChanges( 1, 2 );
-
- $create12 = $changes12['create']; // create item
- $update12 = $changes12['update']; // update item
- $create_update12 = $changes12['create+update']; // merged
create and update
-
- return array(
- array( // #0: empty
- array(), // $changes
- array(), // $expected
- ),
-
- array( // #1: single
- array( $create11 ), // $changes
- array( $create11 ), // $expected
- ),
-
- array( // #2: unrelated
- array( $create11, $update12 ), // $changes
- array( $create11, $update12 ), // $expected
- ),
-
- array( // #3: reversed
- array( $update12, $create11 ), // $changes
- array( $create11, $update12 ), // $expected
- ),
-
- array( // #4: mixed
- array( $create11, $create12, $update11,
$update12, $updateXLink11, $delete11 ), // $changes
- array( $create_update_link11, $create_update12,
$delete11 ), // $expected
- ),
- );
- }
-
- /**
- * @dataProvider provideCoalesceChanges
- */
- public function testCoalesceChanges( $changes, $expected ) {
- $handler = $this->newChangeHandler();
- $coalesced = $handler->coalesceChanges( $changes );
-
- $this->assertEquals( count( $expected ), count( $coalesced ),
"number of changes" );
-
- $i = 0;
- while ( next( $coalesced ) && next( $expected ) ) {
- $this->assertChangeEquals( current( $expected ),
current( $coalesced ), "expected[" . $i++ . "]" );
- }
- }
-
-
- //
==================================================================================
-
- public static function provideHandleChanges() {
+ public function provideHandleChanges() {
$empty = Item::newEmpty();
$empty->setId( new ItemId( 'q55668877' ) );
@@ -779,14 +175,7 @@
$handleChangeCallCount = 0;
$handleChangesCallCount = 0;
- $changeHandler = $this->getMockBuilder(
'Wikibase\Client\Changes\ChangeHandler' )
- ->disableOriginalConstructor()->setMethods( array(
'coalesceChanges', 'handleChange' ) )->getMock();
-
- $changeHandler->expects( $this->once() )
- ->method( 'coalesceChanges' )->will(
$this->returnValue( $changes ) );
-
- $changeHandler->expects( $this->exactly( count( $changes ) ) )
- ->method( 'handleChange' );
+ $changeHandler = $this->newChangeHandler();
$changeHandler->handleChanges( $changes );
@@ -1083,25 +472,6 @@
} ) );
return $usageLookup;
- }
-
- /**
- * @return SiteList
- */
- private function getSiteList() {
- $siteList = $this->getMock( 'SiteList' );
- $siteList->expects( $this->any() )
- ->method( 'getSite' )
- ->will( $this->returnCallback( function( $globalSiteId
) {
- $site = new \MediaWikiSite();
-
- $site->setGlobalId( $globalSiteId );
- $site->setLanguageCode( substr( $globalSiteId,
0, 2 ) );
-
- return $site;
- } ) );
-
- return $siteList;
}
/**
diff --git a/client/tests/phpunit/includes/Changes/ChangeRunCoalescerTest.php
b/client/tests/phpunit/includes/Changes/ChangeRunCoalescerTest.php
new file mode 100644
index 0000000..fdfadd9
--- /dev/null
+++ b/client/tests/phpunit/includes/Changes/ChangeRunCoalescerTest.php
@@ -0,0 +1,422 @@
+<?php
+
+namespace Wikibase\Client\Tests\Changes;
+
+use Diff\Differ\MapDiffer;
+use Wikibase\Change;
+use Wikibase\ChangesTable;
+use Wikibase\Client\Changes\ChangeRunCoalescer;
+use Wikibase\DataModel\Entity\Diff\EntityDiff;
+use Wikibase\DataModel\Entity\Item;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\EntityChange;
+use Wikibase\Test\MockRepository;
+use Wikibase\Test\TestChanges;
+
+/**
+ * @covers Wikibase\Client\Changes\ChangeRunCoalescer
+ *
+ * @group Wikibase
+ * @group WikibaseClient
+ * @group WikibaseChange
+ * @group ChangeHandlerTest
+ *
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class ChangeRunCoalescerTest extends \MediaWikiTestCase {
+
+ private function newCoalescer( array $entities = array() ) {
+ $repo = $this->getMockRepo( $entities );
+
+ $changeFactory = TestChanges::getEntityChangeFactory();
+
+ $coalescer = new ChangeRunCoalescer(
+ $repo,
+ $changeFactory,
+ 'enwiki'
+ );
+
+ return $coalescer;
+ }
+
+ private function getMockRepo( array $entities = array() ) {
+ $repo = new MockRepository();
+
+ // entity 1, revision 11
+ $entity1 = Item::newEmpty();
+ $entity1->setId( new ItemId( 'q1' ) );
+ $entity1->setLabel( 'en', 'one' );
+ $repo->putEntity( $entity1, 11 );
+
+ // entity 1, revision 12
+ $entity1->setLabel( 'de', 'eins' );
+ $repo->putEntity( $entity1, 12 );
+
+ // entity 1, revision 13
+ $entity1->setLabel( 'it', 'uno' );
+ $repo->putEntity( $entity1, 13 );
+
+ // entity 1, revision 1111
+ $entity1->setDescription( 'en', 'the first' );
+ $repo->putEntity( $entity1, 1111 );
+
+ // entity 2, revision 21
+ $entity1 = Item::newEmpty();
+ $entity1->setId( new ItemId( 'q2' ) );
+ $entity1->setLabel( 'en', 'two' );
+ $repo->putEntity( $entity1, 21 );
+
+ // entity 2, revision 22
+ $entity1->setLabel( 'de', 'zwei' );
+ $repo->putEntity( $entity1, 22 );
+
+ // entity 2, revision 23
+ $entity1->setLabel( 'it', 'due' );
+ $repo->putEntity( $entity1, 23 );
+
+ // entity 2, revision 1211
+ $entity1->setDescription( 'en', 'the second' );
+ $repo->putEntity( $entity1, 1211 );
+
+ $this->updateMockRepo( $repo, $entities );
+
+ return $repo;
+ }
+
+ private function updateMockRepo( MockRepository $repo, $entities ) {
+ foreach ( $entities as $id => $siteLinks ) {
+ if ( !( $siteLinks instanceof Entity ) ) {
+ $entity = Item::newEmpty();
+ $entity->setId( new ItemId( $id ) );
+
+ foreach ( $siteLinks as $siteId => $page ) {
+ if ( is_int( $siteId ) ) {
+ $siteIdentifier =
$this->site->getGlobalId();
+ } else {
+ $siteIdentifier = $siteId;
+ }
+
+ $entity->addSiteLink( new SiteLink(
$siteIdentifier, $page ) );
+ }
+ } else {
+ $entity = $siteLinks;
+ }
+
+ $repo->putEntity( $entity );
+ }
+ }
+
+ /**
+ * @param array $values
+ * @param EntityDiff|null $diff
+ *
+ * @return EntityChange
+ */
+ private function makeChange( array $values, EntityDiff $diff = null ) {
+ if ( !isset( $values['info'] ) ) {
+ $values['info'] = array();
+ }
+
+ if ( !isset( $values['info']['metadata'] ) ) {
+ $values['info']['metadata'] = array();
+ }
+
+ if ( !isset( $values['info']['metadata']['rev_id'] ) && isset(
$values['revision_id'] ) ) {
+ $values['info']['metadata']['rev_id'] = $values[
'revision_id' ];
+ }
+
+ if ( !isset( $values['info']['metadata']['user_text'] ) &&
isset( $values['user_id'] ) ) {
+ $values['info']['metadata']['user_text'] = "User" .
$values['user_id'];
+ }
+
+ if ( !isset( $values['info']['metadata']['parent_id'] ) &&
isset( $values['parent_id'] ) ) {
+ $values['info']['metadata']['parent_id'] =
$values['parent_id'];
+ }
+
+ if ( !isset( $values['info']['metadata']['parent_id'] ) ) {
+ $values['info']['metadata']['parent_id'] = 0;
+ }
+
+ $values['info'] = serialize( $values['info'] );
+
+ /* @var EntityChange $change */
+ $table = ChangesTable::singleton();
+ $change = $table->newRow( $values, true );
+
+ if ( $diff ) {
+ $change->setDiff( $diff );
+ }
+
+ return $change;
+ }
+
+ private function makeDiff( $type, $before, $after ) {
+ $differ = new MapDiffer( true );
+
+ $diffOps = $differ->doDiff( $before, $after );
+ $diff = EntityDiff::newForType( $type, $diffOps );
+
+ return $diff;
+ }
+
+ private function assertChangeEquals( Change $expected, Change $actual,
$message = null ) {
+ if ( $message ) {
+ $message .= ': ';
+ } else {
+ $message = 'change.';
+ }
+
+ $this->assertEquals( get_class( $expected ), get_class( $actual
), $message . "class" );
+
+ $this->assertEquals( $expected->getObjectId(),
$actual->getObjectId(), $message . "ObjectId" );
+ $this->assertEquals( $expected->getTime(), $actual->getTime(),
$message . "Time" );
+ $this->assertEquals( $expected->getType(), $actual->getType(),
$message . "Type" );
+ $this->assertEquals( $expected->getUser(), $actual->getUser(),
$message . "User" );
+
+ if ( $expected instanceof EntityChange && $actual instanceof
EntityChange ) {
+ $this->assertEquals( $expected->getAction(),
$actual->getAction(), $message . "Action" );
+ $this->assertArrayEquals( $expected->getMetadata(),
$actual->getMetadata(), false, true );
+ }
+ }
+
+ /**
+ * @todo: move to TestChanges, unify with TestChanges::getChanges()
+ */
+ private function makeTestChanges( $userId, $numericId ) {
+ $prefixedId = 'Q' . $numericId;
+
+ $offset = 100 * $numericId + 1000 * $userId;
+
+ // create with a label and site link set
+ $create = self::makeChange( array(
+ 'id' => $offset + 1,
+ 'type' => 'wikibase-item~add',
+ 'time' => '20130101010101',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 11,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'label' => array( 'en' => 'Test' ),
+ 'links' => array( 'enwiki' => 'Test' ), // old
style sitelink representation
+ )
+ ) );
+
+ // set a label
+ $update = self::makeChange( array(
+ 'id' => $offset + 23,
+ 'type' => 'wikibase-item~update',
+ 'time' => '20130102020202',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 12,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'label' => array( 'de' => 'Test' ),
+ )
+ ) );
+
+ // merged change consisting of $create and $update
+ $create_update = self::makeChange( array(
+ 'id' => null,
+ 'type' => $create->getField('type'), // because the
first change has no parent
+ 'time' => $update->getField('time'), // last change's
timestamp
+ 'object_id' => $update->getField('object_id'),
+ 'revision_id' => $update->getField('revision_id'), //
last changes rev id
+ 'user_id' => $update->getField('user_id'),
+ 'info' => array(
+ 'metadata' => array(
+ 'bot' => 0,
+ 'comment' => 'wikibase-comment-add' //
this assumes a specific 'type'
+ )
+ )
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
+ 'links' => array( 'enwiki' => 'Test' ), // old
style sitelink representation
+ )
+ ) );
+
+ // change link to other wiki
+ $updateXLink = self::makeChange( array(
+ 'id' => $offset + 14,
+ 'type' => 'wikibase-item~update',
+ 'time' => '20130101020304',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 13,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'links' => array( 'dewiki' => array( 'name' =>
'Testen', 'badges' => array() ) ),
+ )
+ ) );
+
+ // merged change consisting of $create, $update and $updateXLink
+ $create_update_link = self::makeChange( array(
+ 'id' => null,
+ 'type' => $create->getField('type'), // because the
first change has no parent
+ 'time' => $updateXLink->getField('time'), // last
change's timestamp
+ 'object_id' => $updateXLink->getField('object_id'),
+ 'revision_id' => $updateXLink->getField('revision_id'),
// last changes rev id
+ 'user_id' => $updateXLink->getField('user_id'),
+ 'info' => array(
+ 'metadata' => array(
+ 'bot' => 0,
+ 'comment' => 'wikibase-comment-add' //
this assumes a specific 'type'
+ )
+ )
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
+ 'links' => array(
+ 'enwiki' => array( 'name' => 'Test' ),
// incomplete new style sitelink representation
+ 'dewiki' => array( 'name' => 'Test' ),
// incomplete new style sitelink representation
+ ),
+ )
+ ) );
+
+ // some other user changed a label
+ $updateX = self::makeChange( array(
+ 'id' => $offset + 12,
+ 'type' => 'wikibase-item~update',
+ 'time' => '20130103030303',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 14,
+ 'user_id' => $userId + 17,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(),
+ array(
+ 'label' => array( 'fr' => array( 'name' =>
'Test', 'badges' => array() ) ),
+ )
+ ) );
+
+ // change link to local wiki
+ $updateLink = self::makeChange( array(
+ 'id' => $offset + 13,
+ 'type' => 'wikibase-item~update',
+ 'time' => '20130102030405',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 17,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(
+ 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q555' ) ) ),
+ ),
+ array(
+ 'links' => array( 'enwiki' => array( 'name' =>
'Spam', 'badges' => array( 'Q12345' ) ) ),
+ )
+ ) );
+
+ // change only badges in link to local wiki
+ $updateLinkBadges = self::makeChange( array(
+ 'id' => $offset + 14,
+ 'type' => 'wikibase-item~update',
+ 'time' => '20130102030405',
+ 'object_id' => $prefixedId,
+ 'revision_id' => $offset + 18,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(
+ 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q555' ) ) ),
+ ),
+ array(
+ 'links' => array( 'enwiki' => array( 'name' =>
'Test', 'badges' => array( 'Q12345' ) ) ),
+ )
+ ) );
+
+ // item deleted
+ $delete = self::makeChange( array(
+ 'id' => $offset + 35,
+ 'type' => 'wikibase-item~remove',
+ 'time' => '20130105050505',
+ 'object_id' => $prefixedId,
+ 'revision_id' => 0,
+ 'user_id' => $userId,
+ ), self::makeDiff( Item::ENTITY_TYPE,
+ array(
+ 'label' => array( 'en' => 'Test', 'de' =>
'Test' ),
+ 'links' => array( 'enwiki' => 'Test', 'dewiki'
=> 'Test' ),
+ ),
+ array()
+ ) );
+
+ return array(
+ 'create' => $create, // create item
+ 'update' => $update, // update item
+ 'create+update' => $create_update, // merged create and
update
+ 'update/other' => $updateX, // update by another
user
+ 'update-link/local' => $updateLink, // change the link
to this client wiki
+ 'update-link/local/basges' => $updateLinkBadges, //
change the link to this client wiki
+ 'update-link/other' => $updateXLink, // change the link
to some other client wiki
+ 'create+update+update-link/other' =>
$create_update_link, // merged create and update and update link to other wiki
+ 'delete' => $delete, // delete item
+ );
+ }
+
+ public function provideCoalesceChanges() {
+ $changes11 = self::makeTestChanges( 1, 1 );
+
+ $create11 = $changes11['create']; // create item
+ $update11 = $changes11['update']; // update item
+ $updateXLink11 = $changes11['update-link/other']; // change the
link to some other client wiki
+ $create_update_link11 =
$changes11['create+update+update-link/other']; // merged create and update and
update link to other wiki
+ $delete11 = $changes11['delete']; // delete item
+
+ $changes12 = self::makeTestChanges( 1, 2 );
+
+ $create12 = $changes12['create']; // create item
+ $update12 = $changes12['update']; // update item
+ $create_update12 = $changes12['create+update']; // merged
create and update
+
+ return array(
+ array( // #0: empty
+ array(), // $changes
+ array(), // $expected
+ ),
+
+ array( // #1: single
+ array( $create11 ), // $changes
+ array( $create11 ), // $expected
+ ),
+
+ array( // #2: unrelated
+ array( $create11, $update12 ), // $changes
+ array( $create11, $update12 ), // $expected
+ ),
+
+ array( // #3: reversed
+ array( $update12, $create11 ), // $changes
+ array( $create11, $update12 ), // $expected
+ ),
+
+ array( // #4: mixed
+ array( $create11, $create12, $update11,
$update12, $updateXLink11, $delete11 ), // $changes
+ array( $create_update_link11, $create_update12,
$delete11 ), // $expected
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCoalesceChanges
+ */
+ public function testCoalesceChanges( $changes, $expected ) {
+ $coalescer = $this->newCoalescer();
+ $coalesced = $coalescer->transformChangeList( $changes );
+
+ $this->assertEquals( count( $expected ), count( $coalesced ),
"number of changes" );
+
+ $i = 0;
+ while ( next( $coalesced ) && next( $expected ) ) {
+ $this->assertChangeEquals( current( $expected ),
current( $coalesced ), "expected[" . $i++ . "]" );
+ }
+ }
+
+}
--
To view, visit https://gerrit.wikimedia.org/r/170995
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ib0085abc4c1cbf8ca162a031a96008840690907b
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Adrian Lang <[email protected]>
Gerrit-Reviewer: Aude <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: JanZerebecki <[email protected]>
Gerrit-Reviewer: Thiemo Mättig (WMDE) <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits