jenkins-bot has submitted this change and it was merged.

Change subject: Filter page purged by usage aspect.
......................................................................


Filter page purged by usage aspect.

Bug: 71352
Change-Id: I9ce6a2e90347f6ce8a9ce9ef4d0a083592211d23
---
M client/includes/Changes/AffectedPagesFinder.php
M client/includes/Changes/ChangeHandler.php
M client/includes/DataAccess/PropertyParserFunction/Runner.php
M client/includes/Usage/EntityUsage.php
M client/includes/Usage/HashUsageAccumulator.php
A client/includes/Usage/PageEntityUsages.php
M client/includes/Usage/ParserOutputUsageAccumulator.php
M client/includes/Usage/Sql/SqlUsageTracker.php
M client/includes/Usage/UsageAccumulator.php
A client/includes/Usage/UsageAspectTransformer.php
M client/includes/Usage/UsageLookup.php
M client/includes/WikibaseClient.php
M client/includes/scribunto/WikibaseLuaBindings.php
M client/tests/phpunit/includes/Changes/AffectedPagesFinderTest.php
M client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
M client/tests/phpunit/includes/DataAccess/PropertyParserFunction/RunnerTest.php
A client/tests/phpunit/includes/Usage/PageEntityUsagesTest.php
M client/tests/phpunit/includes/Usage/UsageAccumulatorContractTester.php
A client/tests/phpunit/includes/Usage/UsageAspectTransformerTest.php
M client/tests/phpunit/includes/Usage/UsageLookupContractTester.php
20 files changed, 1,203 insertions(+), 218 deletions(-)

Approvals:
  Thiemo Mättig (WMDE): Looks good to me, approved
  jenkins-bot: Verified



diff --git a/client/includes/Changes/AffectedPagesFinder.php 
b/client/includes/Changes/AffectedPagesFinder.php
index 39df103..8355d35 100644
--- a/client/includes/Changes/AffectedPagesFinder.php
+++ b/client/includes/Changes/AffectedPagesFinder.php
@@ -2,17 +2,27 @@
 
 namespace Wikibase\Client\Changes;
 
+use ArrayIterator;
 use Diff\DiffOp\Diff\Diff;
+use Diff\DiffOp\DiffOp;
 use Diff\DiffOp\DiffOpAdd;
 use Diff\DiffOp\DiffOpChange;
 use Diff\DiffOp\DiffOpRemove;
+use InvalidArgumentException;
 use Iterator;
 use Title;
 use UnexpectedValueException;
 use Wikibase\Change;
 use Wikibase\Client\Store\TitleFactory;
+use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
+use Wikibase\Client\Usage\UsageAspectTransformer;
 use Wikibase\Client\Usage\UsageLookup;
+use Wikibase\EntityChange;
 use Wikibase\ItemChange;
+use Wikibase\DataModel\Entity\Diff\EntityDiff;
+use Wikibase\DataModel\Entity\Diff\ItemDiff;
+use Wikibase\DataModel\Entity\EntityId;
 use Wikibase\Lib\Store\StorageException;
 use Wikibase\NamespaceChecker;
 
@@ -40,6 +50,11 @@
        private $siteId;
 
        /**
+        * @var string
+        */
+       private $contentLanguageCode;
+
+       /**
         * @var boolean
         */
        private $checkPageExistence;
@@ -54,18 +69,35 @@
         * @param NamespaceChecker $namespaceChecker
         * @param TitleFactory $titleFactory
         * @param string $siteId
+        * @param string $contentLanguageCode
         * @param boolean $checkPageExistence
+        *
+        * @throws InvalidArgumentException
         */
        public function __construct(
                UsageLookup $usageLookup,
                NamespaceChecker $namespaceChecker,
                TitleFactory $titleFactory,
                $siteId,
+               $contentLanguageCode,
                $checkPageExistence = true
        ) {
+               if ( !is_string( $siteId ) ) {
+                       throw new InvalidArgumentException( '$siteId must be a 
string' );
+               }
+
+               if ( !is_string( $contentLanguageCode ) ) {
+                       throw new InvalidArgumentException( 
'$contentLanguageCode must be a string' );
+               }
+
+               if ( !is_bool( $checkPageExistence ) ) {
+                       throw new InvalidArgumentException( 
'$checkPageExistence must be a boolean' );
+               }
+
                $this->usageLookup = $usageLookup;
                $this->namespaceChecker = $namespaceChecker;
                $this->siteId = $siteId;
+               $this->contentLanguageCode = $contentLanguageCode;
                $this->checkPageExistence = $checkPageExistence;
                $this->titleFactory = $titleFactory;
        }
@@ -75,50 +107,112 @@
         *
         * @param Change $change
         *
-        * @return Title[]
+        * @return Iterator<PageEntityUsages>
         */
-       public function getPages( Change $change ) {
-               if ( ! ( $change instanceof ItemChange ) ) {
+       public function getAffectedUsagesByPage( Change $change ) {
+               if ( ! ( $change instanceof EntityChange ) ) {
                        return array();
                }
 
-               $titles = $this->getReferencedPages( $change );
+               $pageUpdates = $this->getAffectedPages( $change );
+               $pageUpdates = $this->filterUpdates( $pageUpdates );
 
-               return $this->filterTitlesToUpdate( $titles );
+               return $pageUpdates;
        }
 
        /**
-        * Returns the pages that need some kind of updating given the change.
+        * @param EntityChange $change
         *
-        * @param ItemChange $change
-        *
-        * @return Title[] the titles of the pages to update. May contain 
duplicates.
+        * @return string[]
         */
-       private function getReferencedPages( ItemChange $change ) {
-               $itemId = $change->getEntityId();
+       public function getChangedAspects( EntityChange $change ) {
+               $aspects = array();
 
-               $pageIds = $this->usageLookup->getPagesUsing( array( $itemId ) 
);
-               $titles = $this->getTitlesFromIDs( $pageIds );
+               /** @var EntityDiff $diff */
+               $diff = $change->getDiff();
+               $remainingDiffOps = count( $diff ); // this is a "deep" count!
 
-               $siteLinkDiff = $change->getSiteLinkDiff();
+               if ( $diff instanceof ItemDiff && 
!$diff->getSiteLinkDiff()->isEmpty() ) {
+                       $sitelinkDiff = $diff->getSiteLinkDiff();
 
-               if ( $this->isRelevantSiteLinkChange( $siteLinkDiff ) ) {
-                       $namesFromDiff = $this->getPagesReferencedInDiff( 
$siteLinkDiff );
-                       $titlesFromDiff = $this->getTitlesFromTexts( 
$namesFromDiff );
+                       $aspects[] = EntityUsage::SITELINK_USAGE;
+                       $remainingDiffOps-= count( $sitelinkDiff );
 
-                       $titles = array_merge( $titles, $titlesFromDiff );
+                       if ( isset( $sitelinkDiff[$this->siteId] ) && 
!$this->isBadgesOnlyChange( $sitelinkDiff[$this->siteId] ) ) {
+                               $aspects[] = EntityUsage::TITLE_USAGE;
+                       }
                }
 
-               return $titles;
+               if ( !$diff->getLabelsDiff()->isEmpty() ) {
+                       $labelDiff = $diff->getLabelsDiff();
+
+                       if ( isset( $labelDiff[$this->contentLanguageCode] ) ) {
+                               $aspects[] = EntityUsage::LABEL_USAGE;
+                               $remainingDiffOps--;
+                       }
+               }
+
+               if ( $remainingDiffOps > 0 ) {
+                       $aspects[] = EntityUsage::OTHER_USAGE;
+               }
+
+               sort( $aspects );
+               return $aspects;
        }
 
        /**
-        * @param Diff $siteLinkDiff
+        * Returns the page updates implied by the given the change.
         *
-        * @return boolean
+        * @param EntityChange $change
+        *
+        * @return Iterator<PageEntityUsages>
         */
-       private function isRelevantSiteLinkChange( Diff $siteLinkDiff ) {
-               return isset( $siteLinkDiff[$this->siteId] ) && 
!$this->isBadgesOnlyChange( $siteLinkDiff );
+       private function getAffectedPages( EntityChange $change ) {
+               $itemId = $change->getEntityId();
+               $changedAspects = $this->getChangedAspects( $change );
+
+               // @todo: more than one item at once!
+               $relevantAspects = array_merge( array( 'X' ), $changedAspects 
); // X implies all!
+               $usages = $this->usageLookup->getPagesUsing( array( $itemId ), 
$relevantAspects );
+
+               // @todo: use iterators throughout!
+               $usages = iterator_to_array( $usages, true );
+
+               $usages = $this->transformAllPageEntityUsages( $usages, 
$itemId, $changedAspects );
+
+               if ( $change instanceof ItemChange && in_array( 
EntityUsage::TITLE_USAGE, $changedAspects ) ) {
+                       $siteLinkDiff = $change->getSiteLinkDiff();
+                       $namesFromDiff = $this->getPagesReferencedInDiff( 
$siteLinkDiff );
+                       $titlesFromDiff = $this->getTitlesFromTexts( 
$namesFromDiff );
+                       $usagesFromDiff = $this->makeVirtualUsages( 
$titlesFromDiff, $itemId, array( EntityUsage::SITELINK_USAGE ) );
+
+                       //FIXME: we can't really merge if $usages is an 
iterator, not an array.
+                       //TODO: Inject $usagesFromDiff "on the fly" while 
streaming other usages.
+                       //NOTE: $usages must pass through mergeUsagesInto for 
re-indexing
+                       $mergedUsages = array();
+                       $this->mergeUsagesInto( $usages, $mergedUsages );
+                       $this->mergeUsagesInto( $usagesFromDiff, $mergedUsages 
);
+                       $usages = $mergedUsages;
+               }
+
+               return new ArrayIterator( $usages );
+       }
+
+       /**
+        * @param PageEntityUsages[] $from PageEntityUsages
+        * @param PageEntityUsages[] &$into Array to merge into
+        */
+       private function mergeUsagesInto( array $from, array &$into ) {
+               /** @var PageEntityUsages $pageEntityUsages */
+               foreach ( $from as $pageEntityUsages ) {
+                       $key = $pageEntityUsages->getPageId();
+
+                       if ( isset( $into[$key] ) ) {
+                               $into[$key]->addUsages( 
$pageEntityUsages->getUsages() );
+                       } else {
+                               $into[$key] = $pageEntityUsages;
+                       }
+               }
        }
 
        /**
@@ -155,28 +249,29 @@
        }
 
        /**
-        * @param Diff $siteLinkDiff
+        * @param DiffOp $siteLinkDiffOp
         *
         * @return boolean
         */
-       private function isBadgesOnlyChange( Diff $siteLinkDiff ) {
-               $siteLinkDiffOp = $siteLinkDiff[$this->siteId];
+       private function isBadgesOnlyChange( DiffOp $siteLinkDiffOp ) {
 
                return ( $siteLinkDiffOp instanceof Diff && !array_key_exists( 
'name', $siteLinkDiffOp ) );
        }
 
        /**
-        * Filters titles to update. This removes duplicates, non-existing 
pages, and pages from
+        * Filters updates based on namespace. This removes duplicates, 
non-existing pages, and pages from
         * namespaces that are not considered "enabled" by the namespace 
checker.
         *
-        * @param Title[] $titles
+        * @param PageEntityUsages[]|Iterator<PageEntityUsages> $updates
         *
-        * @return Title[]
+        * @return Iterator<PageEntityUsages>
         */
-       private function filterTitlesToUpdate( array $titles ) {
+       private function filterUpdates( $updates ) {
                $titlesToUpdate = array();
 
-               foreach ( $titles as $title ) {
+               foreach ( $updates as $pageUpdates ) {
+                       $title = $this->titleFactory->newFromID( 
$pageUpdates->getPageId() );
+
                        if ( $this->checkPageExistence && !$title->exists() ) {
                                continue;
                        }
@@ -187,31 +282,11 @@
                                continue;
                        }
 
-                       // Use the string representation as a key to get rid of 
any duplicates.
-                       $key = $title->getPrefixedDBkey();
-                       $titlesToUpdate[$key] = $title;
+                       $key = $title->getArticleID();
+                       $titlesToUpdate[$key] = $pageUpdates;
                }
 
-               return $titlesToUpdate;
-       }
-
-       /**
-        * @param int[]|Iterator $pageIds
-        *
-        * @return Title[]
-        */
-       private function getTitlesFromIDs( $pageIds ) {
-               $titles = array();
-
-               foreach ( $pageIds as $id ) {
-                       try {
-                               $titles[] = $this->titleFactory->newFromID( $id 
);
-                       } catch ( StorageException $ex ) {
-                               // Page probably got deleted just now. Skip it.
-                       }
-               }
-
-               return $titles;
+               return new ArrayIterator( $titlesToUpdate );
        }
 
        /**
@@ -233,4 +308,42 @@
                return $titles;
        }
 
+       /**
+        * @param Title[] $titles
+        * @param EntityId $entityId
+        * @param string[] $aspects
+        *
+        * @return PageEntityUsages[]
+        */
+       private function makeVirtualUsages( array $titles, EntityId $entityId, 
array $aspects ) {
+               $usagesForItem = array();
+               foreach ( $aspects as $aspect ) {
+                       $usagesForItem[] = new EntityUsage( $entityId, $aspect 
);
+               }
+
+               $usagesPerPage = array();
+               foreach ( $titles as $title ) {
+                       $pid = $title->getArticleID();
+                       $usagesPerPage[$pid] = new PageEntityUsages( $pid, 
$usagesForItem );
+               }
+
+               return $usagesPerPage;
+       }
+
+       private function transformAllPageEntityUsages( array $usages, EntityId 
$entityId, array $changedAspects ) {
+               $aspectTransformer = new UsageAspectTransformer();
+               $aspectTransformer->setRelevantAspects( $entityId, 
$changedAspects );
+
+               $transformed = array();
+
+               foreach( $usages as $key => $usagesOnPage ) {
+                       $transformedUsagesOnPage = 
$aspectTransformer->transformPageEntityUsages( $usagesOnPage );
+                       if ( !$transformedUsagesOnPage->isEmpty() ) {
+                               $transformed[$key] = $transformedUsagesOnPage;
+                       }
+               }
+
+               return $transformed;
+       }
+
 }
diff --git a/client/includes/Changes/ChangeHandler.php 
b/client/includes/Changes/ChangeHandler.php
index 037dfcd..57e3082 100644
--- a/client/includes/Changes/ChangeHandler.php
+++ b/client/includes/Changes/ChangeHandler.php
@@ -6,11 +6,13 @@
 use MWException;
 use Title;
 use Wikibase\Change;
+use Wikibase\Client\Store\TitleFactory;
 use Wikibase\DataModel\Entity\Diff\EntityDiff;
 use Wikibase\DataModel\Entity\Diff\ItemDiff;
 use Wikibase\EntityChange;
 use Wikibase\ItemChange;
 use Wikibase\SiteLinkCommentCreator;
+use Wikibase\Lib\Store\StorageException;
 
 /**
  * Interface for change handling. Whenever a change is detected,
@@ -51,6 +53,16 @@
        const HISTORY_ENTRY_ACTION = 16;
 
        /**
+        * @var AffectedPagesFinder
+        */
+       private $affectedPagesFinder;
+
+       /**
+        * @var TitleFactory
+        */
+       private $titleFactory;
+
+       /**
         * @var PageUpdater $updater
         */
        private $updater;
@@ -61,17 +73,13 @@
        private $changeListTransformer;
 
        /**
-        * @var AffectedPagesFinder
-        */
-       private $affectedPagesFinder;
-
-       /**
         * @var string
         */
        private $localSiteId;
 
        public function __construct(
                AffectedPagesFinder $affectedPagesFinder,
+               TitleFactory $titleFactory,
                PageUpdater $updater,
                ChangeListTransformer $changeListTransformer,
                $localSiteId,
@@ -80,6 +88,7 @@
        ) {
                $this->changeListTransformer = $changeListTransformer;
                $this->affectedPagesFinder = $affectedPagesFinder;
+               $this->titleFactory = $titleFactory;
                $this->updater = $updater;
 
                if ( !is_string( $localSiteId ) ) {
@@ -185,7 +194,8 @@
        public function getPagesToUpdate( Change $change ) {
                wfProfileIn( __METHOD__ );
 
-               $pagesToUpdate = $this->affectedPagesFinder->getPages( $change 
);
+               $usages = $this->affectedPagesFinder->getAffectedUsagesByPage( 
$change );
+               $pagesToUpdate = $this->getTitlesFromPageEntityUsages( $usages 
);
 
                wfProfileOut( __METHOD__ );
 
@@ -193,6 +203,26 @@
        }
 
        /**
+        * @param PageEntityUsages[]|Iterator<PageEntityUsages> $pageIds
+        *
+        * @return Title[]
+        */
+       private function getTitlesFromPageEntityUsages( $usages ) {
+               $titles = array();
+
+               foreach ( $usages as $pageEntityUsages ) {
+                       try {
+                               $pid = $pageEntityUsages->getPageId();
+                               $titles[] = $this->titleFactory->newFromID( 
$pid );
+                       } catch ( StorageException $ex ) {
+                               // Page probably got deleted just now. Skip it.
+                       }
+               }
+
+               return $titles;
+       }
+
+       /**
         * Main entry point for handling changes
         *
         * @since    0.4
diff --git a/client/includes/DataAccess/PropertyParserFunction/Runner.php 
b/client/includes/DataAccess/PropertyParserFunction/Runner.php
index 978904a..1779af9 100644
--- a/client/includes/DataAccess/PropertyParserFunction/Runner.php
+++ b/client/includes/DataAccess/PropertyParserFunction/Runner.php
@@ -75,9 +75,9 @@
                $rendered = $renderer->render( $entityId, $propertyLabelOrId );
                $result = $this->buildResult( $rendered );
 
-               // Track usage of "all" (that is, arbitrary) data from the item.
+               // Track usage of "other" (that is, not label/title/sitelinks) 
data from the item.
                $usageAcc = new ParserOutputUsageAccumulator( 
$parser->getOutput() );
-               $usageAcc->addAllUsage( $entityId );
+               $usageAcc->addOtherUsage( $entityId );
 
                wfProfileOut( __METHOD__ );
                return $result;
diff --git a/client/includes/Usage/EntityUsage.php 
b/client/includes/Usage/EntityUsage.php
index 40725df..ab2fbe7 100644
--- a/client/includes/Usage/EntityUsage.php
+++ b/client/includes/Usage/EntityUsage.php
@@ -43,6 +43,14 @@
        const ALL_USAGE = 'X';
 
        /**
+        * Usage flag indicating that some aspect of the entity was changed
+        * which is not covered by any other usage flag (except "all"). That is,
+        * the specific usage flags together with the "other" flag are 
equivalent
+        * to the "all" flag ( S + T + L + O = X or rather O = X - S - T - L ).
+        */
+       const OTHER_USAGE = 'O';
+
+       /**
         * A list of all valid aspects
         *
         * @var array
@@ -51,6 +59,7 @@
                self::SITELINK_USAGE,
                self::LABEL_USAGE,
                self::TITLE_USAGE,
+               self::OTHER_USAGE,
                self::ALL_USAGE
        );
 
diff --git a/client/includes/Usage/HashUsageAccumulator.php 
b/client/includes/Usage/HashUsageAccumulator.php
index 7302851..7bc0637 100644
--- a/client/includes/Usage/HashUsageAccumulator.php
+++ b/client/includes/Usage/HashUsageAccumulator.php
@@ -41,7 +41,7 @@
        }
 
        /**
-        * Registers the usage an entity's label (in the local content 
language).
+        * @see UsageAccumulator::addLabelUsage
         *
         * @param EntityId $id
         */
@@ -50,17 +50,16 @@
        }
 
        /**
-        * Registers the usage of an entity's local page title, e.g. to refer to
-        * the corresponding page on the local wiki.
+        * @see UsageAccumulator::addTitleUsage
         *
         * @param EntityId $id
         */
-       public function addPageUsage( EntityId $id ) {
+       public function addTitleUsage( EntityId $id ) {
                $this->addUsage( $id, EntityUsage::TITLE_USAGE );
        }
 
        /**
-        * Registers the usage of an entity's sitelinks, e.g. to generate 
language links.
+        * @see UsageAccumulator::addSitelinksUsage
         *
         * @param EntityId $id
         */
@@ -69,8 +68,16 @@
        }
 
        /**
-        * Registers the usage of other or all data of an entity (e.g. when 
accessed
-        * programmatically using Lua).
+        * @see UsageAccumulator::addOtherUsage
+        *
+        * @param EntityId $id
+        */
+       public function addOtherUsage( EntityId $id ) {
+               $this->addUsage( $id, EntityUsage::OTHER_USAGE );
+       }
+
+       /**
+        * @see UsageAccumulator::addAllUsage
         *
         * @param EntityId $id
         */
diff --git a/client/includes/Usage/PageEntityUsages.php 
b/client/includes/Usage/PageEntityUsages.php
new file mode 100644
index 0000000..4eca198
--- /dev/null
+++ b/client/includes/Usage/PageEntityUsages.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Wikibase\Client\Usage;
+
+use InvalidArgumentException;
+use Wikibase\DataModel\Entity\EntityId;
+
+/**
+ * Value object representing the entity usages on a single page.
+ *
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+class PageEntityUsages {
+
+       /**
+        * @var int
+        */
+       private $pageId;
+
+       /**
+        * @var EntityUsage[]
+        */
+       private $usages = array();
+
+       /**
+        * @param int $pageId
+        * @param EntityUsage[] $usages
+        *
+        * @throws InvalidArgumentException
+        */
+       public function __construct( $pageId, array $usages = array() ) {
+               if ( !is_int( $pageId ) || $pageId < 1 ) {
+                       throw new InvalidArgumentException( '$pageId must be an 
integer > 0' );
+               }
+
+               $this->pageId = $pageId;
+               $this->addUsages( $usages );
+       }
+
+       /**
+        * Returns the page this PageEntityUsages object applies to.
+        *
+        * @return int
+        */
+       public function getPageId() {
+               return $this->pageId;
+       }
+
+       /**
+        * @return EntityUsage[] $usages EntityUsage objects keyed and sorted 
by identity string.
+        */
+       public function getUsages() {
+               return $this->usages;
+       }
+
+       /**
+        * @return bool
+        */
+       public function isEmpty() {
+               return empty( $this->usages );
+       }
+
+       /**
+        * @param array $usages
+        *
+        * @throws InvalidArgumentException
+        */
+       public function addUsages( array $usages ) {
+               foreach ( $usages as $usage ) {
+                       if ( !$usage instanceof EntityUsage ) {
+                               throw new InvalidArgumentException( '$usages 
must contain only EntityUsage objects' );
+                       }
+
+                       $key = $usage->getIdentityString();
+                       $this->usages[$key] = $usage;
+               }
+
+               ksort( $this->usages );
+       }
+
+       /**
+        * Collects all usage aspects present on the page.
+        *
+        * string[] aspect names (sorted)
+        */
+       public function getAspects() {
+               $aspects = array();
+
+               foreach ( $this->usages as $usage ) {
+                       $aspect = $usage->getAspect();
+                       $aspects[$aspect] = true;
+               }
+
+               ksort( $aspects );
+               return array_keys( $aspects );
+       }
+
+       /**
+        * @param PageEntityUsages $other
+        *
+        * @return bool
+        */
+       public function equals( PageEntityUsages $other ) {
+               if ( !$other->getPageId() === $this->getPageId() ) {
+                       return false;
+               } elseif( array_keys( $other->getUsages() ) != array_keys( 
$this->getUsages() ) ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+
+       /**
+        * Returns all entities used on the page represented by this 
PageEntityUsages object.
+        *
+        * @return EntityId[] List of EntityIde objects, keyed and sorted by 
their identity string.
+        */
+       public function getEntityIds() {
+               $entityIds = array();
+
+               foreach ( $this->usages as $usage ) {
+                       $id = $usage->getEntityId();
+                       $key = $id->getSerialization();
+                       $entityIds[$key] = $id;
+               }
+
+               ksort( $entityIds );
+               return $entityIds;
+       }
+
+       /**
+        * Returns the aspects used by the given entity on the page
+        * represented by this PageEntityUsages object.
+        *
+        * @param EntityId $id
+        *
+        * @return string[] List of aspect names, sorted.
+        */
+       public function getUsageAspects( EntityId $id ) {
+               $aspects = array();
+
+               foreach ( $this->usages as $usage ) {
+                       if ( $id->equals( $usage->getEntityId() ) ) {
+                               $aspects[] = $usage->getAspect();
+                       }
+               }
+
+               sort( $aspects );
+               return $aspects;
+       }
+
+       public function __toString() {
+               $s = 'Page ' . $this->getPageId() . ' uses (';
+               $s .= implode( '|', array_keys( $this->getUsages() ) );
+               $s .= ')';
+
+               return $s;
+       }
+
+}
diff --git a/client/includes/Usage/ParserOutputUsageAccumulator.php 
b/client/includes/Usage/ParserOutputUsageAccumulator.php
index 4eb5c69..59b1528 100644
--- a/client/includes/Usage/ParserOutputUsageAccumulator.php
+++ b/client/includes/Usage/ParserOutputUsageAccumulator.php
@@ -51,7 +51,7 @@
        }
 
        /**
-        * Registers the usage an entity's label (in the local content 
language).
+        * @see UsageAccumulator::addLabelUsage
         *
         * @param EntityId $id
         */
@@ -60,17 +60,16 @@
        }
 
        /**
-        * Registers the usage of an entity's local page title, e.g. to refer to
-        * the corresponding page on the local wiki.
+        * @see UsageAccumulator::addTitleUsage
         *
         * @param EntityId $id
         */
-       public function addPageUsage( EntityId $id ) {
+       public function addTitleUsage( EntityId $id ) {
                $this->addUsage( $id, EntityUsage::TITLE_USAGE );
        }
 
        /**
-        * Registers the usage of an entity's sitelinks, e.g. to generate 
language links.
+        * @see UsageAccumulator::addSitelinksUsage
         *
         * @param EntityId $id
         */
@@ -79,8 +78,16 @@
        }
 
        /**
-        * Registers the usage of other or all data of an entity (e.g. when 
accessed
-        * programmatically using Lua).
+        * @see UsageAccumulator::addOtherUsage
+        *
+        * @param EntityId $id
+        */
+       public function addOtherUsage( EntityId $id ) {
+               $this->addUsage( $id, EntityUsage::OTHER_USAGE );
+       }
+
+       /**
+        * @see UsageAccumulator::addAllUsage
         *
         * @param EntityId $id
         */
diff --git a/client/includes/Usage/Sql/SqlUsageTracker.php 
b/client/includes/Usage/Sql/SqlUsageTracker.php
index b29c94b..98720a1 100644
--- a/client/includes/Usage/Sql/SqlUsageTracker.php
+++ b/client/includes/Usage/Sql/SqlUsageTracker.php
@@ -10,6 +10,7 @@
 use Iterator;
 use Wikibase\Client\Store\Sql\ConnectionManager;
 use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
 use Wikibase\Client\Usage\UsageLookup;
 use Wikibase\Client\Usage\UsageTracker;
 use Wikibase\Client\Usage\UsageTrackerException;
@@ -221,7 +222,7 @@
         * @param EntityId[] $entityIds
         * @param string[] $aspects
         *
-        * @return Iterator An iterator over page IDs.
+        * @return Iterator<PageEntityUsages> An iterator over entity usages 
grouped by page
         * @throws UsageTrackerException
         */
        public function getPagesUsing( array $entityIds, array $aspects = 
array() ) {
@@ -240,12 +241,12 @@
 
                $res = $db->select(
                        'wbc_entity_usage',
-                       array( 'DISTINCT eu_page_id' ),
+                       array( 'eu_page_id', 'eu_entity_id', 'eu_aspect' ),
                        $where,
                        __METHOD__
                );
 
-               $pages = $this->extractProperty( $res, 'eu_page_id' );
+               $pages = $this->foldRowsIntoPageEntityUsages( $res );
 
                $this->connectionManager->releaseConnection( $db );
 
@@ -254,6 +255,34 @@
        }
 
        /**
+        * @param array|Iterator $rows
+        *
+        * @return PageEntityUsages[]
+        */
+       private function foldRowsIntoPageEntityUsages( $rows ) {
+               $usagesPerPage = array();
+
+               foreach ( $rows as $row ) {
+                       $pageId = (int)$row->eu_page_id;
+
+                       if ( isset( $usagesPerPage[$pageId] ) ) {
+                               $pageEntityUsages = $usagesPerPage[$pageId];
+                       } else {
+                               $pageEntityUsages = new PageEntityUsages( 
$pageId );
+                       }
+
+                       $entityId = $this->idParser->parse( $row->eu_entity_id 
);
+                       $usage = new EntityUsage( $entityId, $row->eu_aspect );
+                       $pageEntityUsages->addUsages( array( $usage ) );
+
+                       $usagesPerPage[$pageId] = $pageEntityUsages;
+               }
+
+               return $usagesPerPage;
+       }
+
+
+       /**
         * @see UsageTracker::getUnusedEntities
         *
         * @param EntityId[] $entityIds
diff --git a/client/includes/Usage/UsageAccumulator.php 
b/client/includes/Usage/UsageAccumulator.php
index 364668c..d6ff542 100644
--- a/client/includes/Usage/UsageAccumulator.php
+++ b/client/includes/Usage/UsageAccumulator.php
@@ -25,7 +25,7 @@
         *
         * @param EntityId $id
         */
-       public function addPageUsage( EntityId $id );
+       public function addTitleUsage( EntityId $id );
 
        /**
         * Registers the usage of an entity's sitelinks, e.g. to generate 
language links.
@@ -35,7 +35,16 @@
        public function addSiteLinksUsage( EntityId $id );
 
        /**
-        * Registers the usage of other or all data of an entity (e.g. when 
accessed
+        * Registers the usage of other (i.e. not label, sitelink, or title) of 
an
+        * entity (e.g. access to statements or labels in labels a language 
other
+        * than the content language).
+        *
+        * @param EntityId $id
+        */
+       public function addOtherUsage( EntityId $id );
+
+       /**
+        * Registers the usage of any/all data of an entity (e.g. when accessed
         * programmatically using Lua).
         *
         * @param EntityId $id
diff --git a/client/includes/Usage/UsageAspectTransformer.php 
b/client/includes/Usage/UsageAspectTransformer.php
new file mode 100644
index 0000000..83df0ad
--- /dev/null
+++ b/client/includes/Usage/UsageAspectTransformer.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Wikibase\Client\Usage;
+
+use Wikibase\DataModel\Entity\EntityId;
+
+/**
+ * Transforms usage aspect based on a filter of aspects relevant in some 
context.
+ * Relevant aspects for each entity are collected using the 
setRelevantAspects()
+ * method.
+ *
+ * @example: If a page uses the "label" (L) and "title" (T) aspects of item 
Q1, a
+ * UsageAspectTransformer that was set up to consider the label aspect of Q1
+ * to be relevant will transform the usage Q1#L + Q1#T to the "relevant" usage 
Q1#L.
+ *
+ * @example: The "all" (X) aspect is treated specially: If a page uses the X 
aspect,
+ * a  UsageAspectTransformer that was constructed to consider e.g. the label 
and title
+ * aspects of Q1 to be relevant will transform the usage Q1#X to the "relevant"
+ * usage Q1#L + Q1#T. Conversely, if a page uses the "sitelink" (S) aspect, a
+ * UsageAspectTransformer that was constructed to consider all (X) usages 
relevant
+ * will keep the usage Q1#S usage as "relevant".
+ *
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+class UsageAspectTransformer {
+
+       /**
+        * @var array[] An associative array, mapping entity IDs to lists of 
aspect names.
+        */
+       private $relevantAspectsPerEntity;
+
+       /**
+        * @param EntityId $entityId
+        * @param array[] $aspects
+        */
+       public function setRelevantAspects( EntityId $entityId, array $aspects 
) {
+               $key = $entityId->getSerialization();
+               $this->relevantAspectsPerEntity[$key] = $aspects;
+       }
+
+       /**
+        * @param EntityId $entityId
+        *
+        * @return string[]
+        */
+       public function getRelevantAspects( EntityId $entityId ) {
+               $key = $entityId->getSerialization();
+               return isset( $this->relevantAspectsPerEntity[$key] ) ? 
$this->relevantAspectsPerEntity[$key] : array();
+       }
+
+       /**
+        * Gets EntityUsage objects for each aspect in $aspects that is 
relevant according to
+        * getRelevantAspects( $entityId ).
+        *
+        * @example: If was called with setRelevantAspects( $q3, array( 'T', 
'L' ) ),
+        * getFilteredUsages( $q3, array( 'S', 'L' ) ) will return EntityUsage( 
$q3, 'L'),
+        * while getFilteredUsages( $q3, array( 'X' ) ) will return 
EntityUsage( $q3, 'T')
+        * and EntityUsage( $q3, 'L').
+        *
+        * @param EntityId $entityId
+        * @param string[] $aspects
+        *
+        * @return EntityUsage[] $usages;
+        */
+       public function getFilteredUsages( EntityId $entityId, array $aspects ) 
{
+               $relevant = $this->getRelevantAspects( $entityId );
+               $effectiveAspects = $this->getFilteredAspects( $aspects, 
$relevant );
+
+               return $this->buildEntityUsages( $entityId, $effectiveAspects );
+       }
+
+       /**
+        * Transforms the entity usages from $pageEntityUsages according to the 
relevant
+        * aspects defined by calling setRelevantAspects(). A new 
PageEntityUsages
+        * containing the filtered usage list is returned.
+        *
+        * @see getFilteredUsages()
+        *
+        * @param PageEntityUsages $pageEntityUsages
+        *
+        * @return PageEntityUsages
+        */
+       public function transformPageEntityUsages( PageEntityUsages 
$pageEntityUsages ) {
+               $entityIds = $pageEntityUsages->getEntityIds();
+               $transformedPageEntityUsages = new PageEntityUsages( 
$pageEntityUsages->getPageId(), array() );
+
+               foreach ( $entityIds as $id ) {
+                       $aspects = $pageEntityUsages->getUsageAspects( $id );
+                       $usages = $this->getFilteredUsages( $id, $aspects );
+                       $transformedPageEntityUsages->addUsages( $usages );
+               }
+
+               return $transformedPageEntityUsages;
+       }
+
+       /**
+        * @param EntityId $entityId
+        * @param array[] $aspects
+        *
+        * @return EntityUsage[]
+        */
+       private function buildEntityUsages( EntityId $entityId, array $aspects 
) {
+               $usages = array();
+
+               foreach ( $aspects as $aspect ) {
+                       $entityUsage = new EntityUsage( $entityId, $aspect );
+                       $key = $entityUsage->getIdentityString();
+
+                       $usages[$key] = $entityUsage;
+               }
+
+               ksort( $usages );
+               return $usages;
+       }
+
+       /**
+        * Filter $aspects based on the aspects provided by $relevant, 
according to the rules
+        * defined for combining aspects (see class level documentation).
+        *
+        * @note This returns the intersection of $aspects and $relevant,
+        * except if on of the list contains the ALL_USAGE code (X).
+        * If X is present in $aspects, this method will return $relevant (if 
"all" is in the
+        * base set, the filtered set will be the filter itself).
+        * If X is present in $relevant, this method returns $aspects (if all 
aspects are relevant,
+        * nothing is filtered out).
+        *
+        * @param string[] $aspects
+        * @param string[] $relevant
+        *
+        * @return string[]
+        */
+       private function getFilteredAspects( array $aspects, array $relevant ) {
+               if ( empty( $aspects ) || empty( $relevant ) ) {
+                       return array();
+               }
+
+               if ( in_array( 'X', $aspects ) ) {
+                       return $relevant;
+               } elseif ( in_array( 'X', $relevant ) ) {
+                       return $aspects;
+               }
+
+               return array_intersect( $aspects, $relevant );
+       }
+
+}
diff --git a/client/includes/Usage/UsageLookup.php 
b/client/includes/Usage/UsageLookup.php
index 6c93d3d..5f5630d 100644
--- a/client/includes/Usage/UsageLookup.php
+++ b/client/includes/Usage/UsageLookup.php
@@ -32,7 +32,7 @@
         * @param string[] $aspects Which aspects to consider (if omitted, all 
aspects are considered).
         * Use the EntityUsage::XXX_USAGE constants to represent aspects.
         *
-        * @return Iterator An iterator over the IDs of pages using any of the 
given entities.
+        * @return Iterator<PageEntityUsages> An iterator over PageEntityUsages 
of pages using any of the given entities.
         *         If $aspects is given, only usages of these aspects are 
included in the result.
         * @throws UsageTrackerException
         */
diff --git a/client/includes/WikibaseClient.php 
b/client/includes/WikibaseClient.php
index 7ed0f92..ae0dadc 100644
--- a/client/includes/WikibaseClient.php
+++ b/client/includes/WikibaseClient.php
@@ -729,7 +729,7 @@
                        $this->getNamespaceChecker(),
                        new TitleFactory(),
                        $this->settings->getSetting( 'siteGlobalID' ),
-                       true
+                       $this->getContentLanguage()->getCode()
                );
        }
 
@@ -741,6 +741,7 @@
 
                return new ChangeHandler(
                        $this->getAffectedPagesFinder(),
+                       new TitleFactory(),
                        new WikiPageUpdater(),
                        new ChangeRunCoalescer(
                                $this->getStore()->getEntityRevisionLookup(),
diff --git a/client/includes/scribunto/WikibaseLuaBindings.php 
b/client/includes/scribunto/WikibaseLuaBindings.php
index d1b4d8a..028c91c 100644
--- a/client/includes/scribunto/WikibaseLuaBindings.php
+++ b/client/includes/scribunto/WikibaseLuaBindings.php
@@ -9,6 +9,7 @@
 use Wikibase\DataModel\Entity\EntityDocument;
 use Wikibase\DataModel\Entity\EntityIdParser;
 use Wikibase\DataModel\Entity\EntityIdParsingException;
+use Wikibase\DataModel\Entity\Item;
 use Wikibase\DataModel\Entity\ItemId;
 use Wikibase\DataModel\Entity\PropertyDataTypeLookup;
 use Wikibase\LanguageFallbackChainFactory;
@@ -234,7 +235,7 @@
                        return null;
                }
 
-               $this->usageAccumulator->addPageUsage( $id );
+               $this->usageAccumulator->addTitleUsage( $id );
                return $id->getSerialization();
        }
 
@@ -303,12 +304,13 @@
                        return null;
                }
 
+               /** @var Item $item */
                $item = $this->entityLookup->getEntity( $itemId );
                if ( !$item || !$item->getSiteLinkList()->hasLinkWithSiteId( 
$this->siteId ) ) {
                        return null;
                }
 
-               $this->usageAccumulator->addPageUsage( $itemId );
+               $this->usageAccumulator->addTitleUsage( $itemId );
                return $item->getSiteLinkList()->getBySiteId( $this->siteId 
)->getPageName();
        }
 
diff --git a/client/tests/phpunit/includes/Changes/AffectedPagesFinderTest.php 
b/client/tests/phpunit/includes/Changes/AffectedPagesFinderTest.php
index 20b829c..5d6d768 100644
--- a/client/tests/phpunit/includes/Changes/AffectedPagesFinderTest.php
+++ b/client/tests/phpunit/includes/Changes/AffectedPagesFinderTest.php
@@ -6,6 +6,8 @@
 use Title;
 use Wikibase\Client\Changes\AffectedPagesFinder;
 use Wikibase\Client\Store\TitleFactory;
+use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
 use Wikibase\DataModel\Entity\Item;
 use Wikibase\DataModel\Entity\ItemId;
 use Wikibase\DataModel\SiteLink;
@@ -23,10 +25,14 @@
  *
  * @licence GNU GPL v2+
  * @author Katie Filbert < [email protected] >
+ * @author Daniel Kinzler
  */
 class AffectedPagesFinderTest extends \MediaWikiTestCase {
 
        /**
+        * Returns a TitleFactory that generates Title objects based on the 
assumption
+        * that a page's title is the same as the page's article ID (in decimal 
notation).
+        *
         * @return TitleFactory
         */
        private function getTitleFactory() {
@@ -35,14 +41,9 @@
                $titleFactory->expects( $this->any() )
                        ->method( 'newFromID' )
                        ->will( $this->returnCallback( function( $id ) {
-                               switch ( $id ) {
-                                       case 1:
-                                               return Title::makeTitle( 
NS_MAIN, 'Berlin' );
-                                       case 2:
-                                               return Title::makeTitle( 
NS_MAIN, 'Rome' );
-                                       default:
-                                               throw new StorageException( 
'Unknown ID: ' . $id );
-                               }
+                               $title = Title::makeTitle( NS_MAIN, "$id" );
+                               $title->resetArticleID( $id );
+                               return $title;
                        } ) );
 
                $titleFactory->expects( $this->any() )
@@ -54,16 +55,14 @@
                                        throw new StorageException( 'Bad title 
text: ' . $text );
                                }
 
+                               $title->resetArticleID( intval( $text ) );
                                return $title;
                        } ) );
 
                return $titleFactory;
        }
 
-       /**
-        * @dataProvider getPagesProvider
-        */
-       public function testGetPages( array $expected, array $usage, ItemChange 
$change, $message ) {
+       private function getAffectedPagesFinder( array $usage ) {
                $usageLookup = $this->getMock( 
'Wikibase\Client\Usage\UsageLookup' );
 
                $usageLookup->expects( $this->any() )
@@ -71,7 +70,7 @@
                        ->will( $this->returnValue( new ArrayIterator( $usage ) 
) );
 
                $namespaceChecker = $this->getMockBuilder( 
'Wikibase\NamespaceChecker' )
-                                                       
->disableOriginalConstructor()->getMock();
+                       ->disableOriginalConstructor()->getMock();
 
                $namespaceChecker->expects( $this->any() )
                        ->method( 'isWikibaseEnabled' )
@@ -79,157 +78,363 @@
 
                $titleFactory = $this->getTitleFactory();
 
-               $referencedPagesFinder = new AffectedPagesFinder(
+               $affectedPagesFinder = new AffectedPagesFinder(
                        $usageLookup,
                        $namespaceChecker,
                        $titleFactory,
                        'enwiki',
+                       'en',
                        false
                );
 
-               $referencedPages = $referencedPagesFinder->getPages( $change );
-               $referencedPageNames = $this->getPrefixedTitles( 
$referencedPages );
-               $expectedPageNames = $this->getPrefixedTitles( $expected );
-
-               $this->assertEquals( $expectedPageNames, $referencedPageNames, 
$message );
+               return $affectedPagesFinder;
        }
 
-       public function getPagesProvider() {
-               $berlin = Title::makeTitle( NS_MAIN, 'Berlin' );
-               $rome = Title::makeTitle( NS_MAIN, 'Rome' );
-
+       public function getChangedAspectsProvider() {
                $changeFactory = TestChanges::getEntityChangeFactory();
-
                $cases = array();
 
-               $cases[] = array(
-                       array( $berlin ),
-                       array(),
+               $q1 = new ItemId( 'Q1' );
+               $q2 = new ItemId( 'Q2' );
+
+               $cases['create linked item Q1'] = array(
+                       array( EntityUsage::SITELINK_USAGE, 
EntityUsage::TITLE_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::ADD,
                                null,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Berlin' ) )
-                       ),
-                       'created item with site link to client'
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) )
+                       )
                );
 
-               $cases[] = array(
-                       array( $berlin ),
-                       array(),
+               $cases['unlink item Q1'] = array(
+                       array( EntityUsage::SITELINK_USAGE, 
EntityUsage::TITLE_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::UPDATE,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Berlin' ) ),
-                               $this->getEmptyItem()
-                       ),
-                       'removed site link to client'
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getEmptyItem( $q1 )
+                       )
                );
 
-               $cases[] = array(
-                       array( $rome ),
-                       array(),
+               $cases['link item Q2'] = array(
+                       array( EntityUsage::SITELINK_USAGE, 
EntityUsage::TITLE_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::UPDATE,
-                               $this->getEmptyItem(),
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Rome' ) )
-                       ),
-                       'added site link to client'
+                               $this->getEmptyItem( $q2 ),
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) )
+                       )
                );
 
-               $cases[] = array(
-                       array( $berlin, $rome ),
-                       array(),
+               $cases['change link of Q1'] = array(
+                       array( EntityUsage::SITELINK_USAGE, 
EntityUsage::TITLE_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::UPDATE,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Rome' ) ),
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Berlin' ) )
-                       ),
-                       'changed client site link'
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '2' ) )
+                       )
                );
 
-               $cases[] = array(
-                       array( $rome ),
-                       array(),
+               $cases['delete linked item Q2'] = array(
+                       array( EntityUsage::SITELINK_USAGE, 
EntityUsage::TITLE_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::REMOVE,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Rome' ) ),
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) ),
                                null
                        ),
                        'item connected to client was deleted'
                );
 
-               $cases[] = array(
-                       array( $rome ),
-                       array( 2 ),
+               $cases['add another sitelink to Q2'] = array(
+                       array( EntityUsage::SITELINK_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::UPDATE,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Rome' ) ),
-                               $this->getItemWithSiteLinks( array(
-                                       'enwiki' => 'Rome',
-                                       'itwiki' => 'Roma',
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) ),
+                               $this->getItemWithSiteLinks( $q2, array(
+                                       'enwiki' => '2',
+                                       'itwiki' => 'DUE',
                                ) )
-                       ),
-                       'added site link on connected item'
+                       )
                );
 
-               $cases[] = array(
-                       array(),
-                       array(),
+               $cases['other language label change on Q1'] = array(
+                       array( EntityUsage::OTHER_USAGE ),
                        $changeFactory->newFromUpdate(
                                ItemChange::UPDATE,
-                               $this->getEmptyItem(),
-                               $this->getItemWithLabel( 'de', 'Berlin' )
-                       ),
-                       'unrelated label change'
+                               $this->getEmptyItem( $q1 ),
+                               $this->getItemWithLabel( $q1, 'de', 'EINS' )
+                       )
                );
 
-               $connectedItem = $this->getItemWithSiteLinks( array( 'enwiki' 
=> 'Berlin' ) );
-               $connectedItemWithLabel = $this->getItemWithSiteLinks( array( 
'enwiki' => 'Berlin' ) );
-               $connectedItemWithLabel->setLabel( 'enwiki', 'Berlin' );
-
-               $cases[] = array(
-                       array( $berlin ),
-                       array( 1 ),
-                       $changeFactory->newFromUpdate( ItemChange::UPDATE, 
$connectedItem, $connectedItemWithLabel ),
-                       'connected item label change'
+               $cases['local label change on Q1 (used by Q2)'] = array(
+                       array( EntityUsage::LABEL_USAGE ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q1 ),
+                               $this->getItemWithLabel( $q1, 'en', 'ONE' )
+                       )
                );
 
-               $itemWithBadge = $this->getEmptyItem();
                $badges = array( new ItemId( 'Q34' ) );
-               $itemWithBadge->addSiteLink( new SiteLink( 'enwiki', 'Rome', 
$badges  ) );
-
-               $cases[] = array(
-                       array(),
-                       array(),
+               $cases['badge only change on Q1'] = array(
+                       array( EntityUsage::SITELINK_USAGE ),
                        $changeFactory->newFromUpdate( ItemChange::UPDATE,
-                               $this->getItemWithSiteLinks( array( 'enwiki' => 
'Rome' ) ),
-                               $itemWithBadge ),
-                       'badge change'
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ), $badges ) )
                );
 
                return $cases;
        }
 
        /**
+        * @dataProvider getChangedAspectsProvider
+        */
+       public function testGetChangedAspects( array $expected, ItemChange 
$change ) {
+               $referencedPagesFinder = $this->getAffectedPagesFinder( array() 
);
+
+               $actual = $referencedPagesFinder->getChangedAspects( $change );
+
+               sort( $expected );
+               sort( $actual );
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function getAffectedUsagesByPageProvider() {
+               $changeFactory = TestChanges::getEntityChangeFactory();
+
+               $q1 = new ItemId( 'Q1' );
+               $q2 = new ItemId( 'Q2' );
+
+               $q1SitelinkUsage = new EntityUsage( $q1, 
EntityUsage::SITELINK_USAGE );
+               $q2SitelinkUsage = new EntityUsage( $q2, 
EntityUsage::SITELINK_USAGE );
+               $q2AllUsage = new EntityUsage( $q2, EntityUsage::ALL_USAGE );
+               $q2OtherUsage = new EntityUsage( $q2, EntityUsage::OTHER_USAGE 
);
+
+               $q1LabelUsage = new EntityUsage( $q1, EntityUsage::LABEL_USAGE 
);
+               $q2LabelUsage = new EntityUsage( $q2, EntityUsage::LABEL_USAGE 
);
+
+               $q1TitleUsage = new EntityUsage( $q1, EntityUsage::TITLE_USAGE 
);
+               $q2TitleUsage = new EntityUsage( $q2, EntityUsage::TITLE_USAGE 
);
+
+               $page1Q1Usages = new PageEntityUsages( 1, array(
+                       $q1SitelinkUsage,
+               ) );
+
+               $page2Q1Usages = new PageEntityUsages( 2, array(
+                       $q1LabelUsage,
+                       $q1TitleUsage,
+               ) );
+
+               $page1Q2Usages = new PageEntityUsages( 1, array(
+                       $q2LabelUsage,
+                       $q2TitleUsage,
+               ) );
+
+               $page2Q2Usages = new PageEntityUsages( 2, array(
+                       $q2AllUsage,
+               ) );
+
+               // Cases
+               // item with link created
+               // item with link deleted
+               // link added
+               // removed added
+               // link changed
+               // direct aspect match
+               // no aspect match
+               // all matches any
+               // any matches all
+
+               $cases = array();
+
+               $cases['create linked item Q1'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( 
$q1SitelinkUsage ) ),
+                       ),
+                       array(), // No usages recorded yet
+                       $changeFactory->newFromUpdate(
+                               ItemChange::ADD,
+                               null,
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) )
+                       )
+               );
+
+               $cases['unlink item Q1'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( 
$q1SitelinkUsage ) ),
+                               new PageEntityUsages( 2, array( $q1TitleUsage ) 
),
+                       ),
+                       array( $page1Q1Usages, $page2Q1Usages ), // "1" was 
recorded to be linked to Q1 and the local title used on page "2"
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getEmptyItem( $q1 )
+                       )
+               );
+
+               $cases['link item Q2'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( $q2TitleUsage ) 
),
+                               new PageEntityUsages( 2, array( $q2TitleUsage, 
$q2SitelinkUsage ) ),
+                       ),
+                       array( $page1Q2Usages, $page2Q2Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q2 ),
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) )
+                       )
+               );
+
+               $cases['change link of Q1, with NO prior record'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( 
$q1SitelinkUsage ) ),
+                               new PageEntityUsages( 2, array( 
$q1SitelinkUsage ) ),
+                       ),
+                       array(),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '2' ) )
+                       )
+               );
+
+               $cases['change link of Q1, with prior record'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( 
$q1SitelinkUsage ) ),
+                               new PageEntityUsages( 2, array( 
$q1SitelinkUsage, $q1TitleUsage ) ),
+                       ),
+                       array( $page1Q1Usages, $page2Q1Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '2' ) )
+                       )
+               );
+
+               $badges = array( new ItemId( 'Q34' ) );
+               $cases['badge only change on Q1'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( 
$q1SitelinkUsage ) ),
+                       ),
+                       array( $page1Q1Usages, $page2Q1Usages ),
+                       $changeFactory->newFromUpdate( ItemChange::UPDATE,
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ) ),
+                               $this->getItemWithSiteLinks( $q1, array( 
'enwiki' => '1' ), $badges ) )
+               );
+
+               $cases['delete linked item Q2'] = array(
+                       array(
+                               new PageEntityUsages( 1, array( $q2TitleUsage ) 
),
+                               new PageEntityUsages( 2, array( $q2TitleUsage, 
$q2SitelinkUsage ) ),
+                       ),
+                       array( $page1Q2Usages, $page2Q2Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::REMOVE,
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) ),
+                               null
+                       ),
+                       'item connected to client was deleted'
+               );
+
+               $cases['add another sitelink to Q2'] = array(
+                       array(
+                               new PageEntityUsages( 2, array( 
$q2SitelinkUsage ) ),
+                       ),
+                       array( $page2Q2Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getItemWithSiteLinks( $q2, array( 
'enwiki' => '2' ) ),
+                               $this->getItemWithSiteLinks( $q2, array(
+                                       'enwiki' => '2',
+                                       'itwiki' => 'DUE',
+                               ) )
+                       )
+               );
+
+               $cases['other language label change on Q1 (not used on any 
page)'] = array(
+                       array(),
+                       array( $page1Q1Usages, $page2Q1Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q1 ),
+                               $this->getItemWithLabel( $q1, 'de', 'EINS' )
+                       )
+               );
+
+               $cases['other language label change on Q2 (used on page 2)'] = 
array(
+                       array(
+                               new PageEntityUsages( 2, array( $q2OtherUsage ) 
),
+                       ),
+                       array( $page1Q2Usages, $page2Q2Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q2 ),
+                               $this->getItemWithLabel( $q2, 'de', 'EINS' )
+                       )
+               );
+
+               $cases['local label change on Q1 (used by page 2)'] = array(
+                       array(
+                               new PageEntityUsages( 2, array( $q1LabelUsage ) 
),
+                       ),
+                       array( $page1Q1Usages, $page2Q1Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q1 ),
+                               $this->getItemWithLabel( $q1, 'en', 'ONE' )
+                       )
+               );
+
+               $cases['label change on Q2 (used by page 1 and page 2)'] = 
array(
+                       array(
+                               new PageEntityUsages( 1, array( $q2LabelUsage ) 
),
+                               new PageEntityUsages( 2, array( $q2LabelUsage ) 
),
+                       ),
+                       array( $page1Q2Usages, $page2Q2Usages ),
+                       $changeFactory->newFromUpdate(
+                               ItemChange::UPDATE,
+                               $this->getEmptyItem( $q2 ),
+                               $this->getItemWithLabel( $q2, 'en', 'TWO' )
+                       )
+               );
+
+               return $cases;
+       }
+
+       /**
+        * @dataProvider getAffectedUsagesByPageProvider
+        */
+       public function testGetAffectedUsagesByPage( array $expected, array 
$usage, ItemChange $change ) {
+               $referencedPagesFinder = $this->getAffectedPagesFinder( $usage 
);
+
+               $actual = $referencedPagesFinder->getAffectedUsagesByPage( 
$change );
+
+               $this->assertPageEntityUsages( $expected, $actual );
+       }
+
+       /**
+        * @param ItemId $id
+        *
         * @return Item
         */
-       private function getEmptyItem() {
+       private function getEmptyItem( ItemId $id ) {
                $item = Item::newEmpty();
-               $item->setId( 2 );
+               $item->setId( $id );
 
                return $item->copy();
        }
 
        /**
+        * @param ItemId $id
         * @param string[] $links
+        * @param ItemId[] $badges
         *
         * @return Item
         */
-       private function getItemWithSiteLinks( array $links ) {
-               $item = $this->getEmptyItem();
+       private function getItemWithSiteLinks( ItemId $id, array $links, array 
$badges = array() ) {
+               $item = $this->getEmptyItem( $id );
 
                foreach( $links as $siteId => $page ) {
                        $item->addSiteLink(
-                               new SiteLink( $siteId, $page )
+                               new SiteLink( $siteId, $page, $badges )
                        );
                }
 
@@ -237,28 +442,40 @@
        }
 
        /**
+        * @param ItemId $id
         * @param string $languageCode
         * @param string $label
         *
         * @return Item
         */
-       private function getItemWithLabel( $languageCode, $label ) {
-               $item = $this->getEmptyItem();
+       private function getItemWithLabel( ItemId $id, $languageCode, $label ) {
+               $item = $this->getEmptyItem( $id );
                $item->setLabel( $languageCode, $label );
 
                return $item;
        }
 
        /**
-        * @param Title[] $titles
+        * @param PageEntityUsages[]|Iterator<PageEntityUsages> $usagesPerPage
         *
-        * @return string[]
+        * @return PageEntityUsages[]
         */
-       private function getPrefixedTitles( array $titles ) {
-               return array_values(
-                       array_map( function( Title $title ) {
-                               return $title->getPrefixedText();
-                       }, $titles )
+       private function getPageEntityUsageStrings( $usagesPerPage ) {
+               $strings = array();
+
+               foreach ( $usagesPerPage as $pageUsages ) {
+                       $strings[] = "$pageUsages";
+               }
+
+               sort( $strings );
+               return $strings;
+       }
+
+       private function assertPageEntityUsages( $expected, $actual, $message = 
'' ) {
+               $this->assertEquals(
+                       $this->getPageEntityUsageStrings( $expected ),
+                       $this->getPageEntityUsageStrings( $actual ),
+                       $message
                );
        }
 
diff --git a/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php 
b/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
index 57b5d06..e300e7f 100644
--- a/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
+++ b/client/tests/phpunit/includes/Changes/ChangeHandlerTest.php
@@ -10,6 +10,8 @@
 use Wikibase\Client\Changes\ChangeHandler;
 use Wikibase\Client\Changes\PageUpdater;
 use Wikibase\Client\Store\TitleFactory;
+use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
 use Wikibase\Client\Usage\UsageLookup;
 use Wikibase\Client\WikibaseClient;
 use Wikibase\DataModel\Entity\Entity;
@@ -71,11 +73,13 @@
                        $namespaceChecker,
                        $titleFactory,
                        'enwiki',
+                       'en',
                        false
                );
 
                $handler = new ChangeHandler(
                        $affectedPagesFinder,
+                       $titleFactory,
                        $updater ? : new MockPageUpdater(),
                        $transformer,
                        'enwiki',
@@ -411,6 +415,7 @@
         */
        private function getTitleFactory( array $entities ) {
                $titlesById = $this->getFakePageIdMap( $entities );
+               $pageIdsByTitle = array_flip( $titlesById );
 
                $titleFactory = $this->getMock( 
'Wikibase\Client\Store\TitleFactory' );
 
@@ -426,11 +431,17 @@
 
                $titleFactory->expects( $this->any() )
                        ->method( 'newFromText' )
-                       ->will( $this->returnCallback( function( $text, 
$defaultNs = NS_MAIN ) {
+                       ->will( $this->returnCallback( function( $text, 
$defaultNs = NS_MAIN ) use ( $pageIdsByTitle ) {
                                $title = Title::newFromText( $text, $defaultNs 
);
 
                                if ( !$title ) {
                                        throw new StorageException( 'Bad title 
text: ' . $text );
+                               }
+
+                               if ( isset( $pageIdsByTitle[$text] ) ) {
+                                       $title->resetArticleID( 
$pageIdsByTitle[$text] );
+                               } else {
+                                       throw new StorageException( 'Unknown 
title text: ' . $text );
                                }
 
                                return $title;
@@ -458,11 +469,19 @@
                                        $pages = array();
 
                                        foreach ( $ids as $id ) {
+                                               if ( !( $id instanceof ItemId ) 
) {
+                                                       continue;
+                                               }
+
                                                $links = 
$siteLinklookup->getSiteLinksForItem( $id );
                                                foreach ( $links as $link ) {
                                                        if ( $link->getSiteId() 
== $site->getGlobalId() ) {
                                                                // we use the 
numeric item id as the fake page id of the local page!
-                                                               $pages[] = 
$id->getNumericId();
+                                                               $usages = array(
+                                                                       new 
EntityUsage( $id, EntityUsage::SITELINK_USAGE ),
+                                                                       new 
EntityUsage( $id, EntityUsage::LABEL_USAGE )
+                                                               );
+                                                               $pages[] = new 
PageEntityUsages( $id->getNumericId(), $usages );
                                                        }
                                                }
                                        }
@@ -526,7 +545,7 @@
                        array( // #6
                                $changes['set-de-label'],
                                array( 'q100' => array( 'enwiki' => 'Emmy2' ) ),
-                               array( 'Emmy2' )
+                               array(), // For the dummy page, only label and 
sitelink usage is defined.
                        ),
                        array( // #7
                                $changes['set-de-label'],
@@ -538,22 +557,26 @@
                                array( 'q100' => array( 'enwiki' => 'Emmy2' ) ),
                                array( 'Emmy2' )
                        ),
+                       array( // #8
+                               $changes['set-en-label'],
+                               array( 'q100' => array( 'enwiki' => 
'User:Emmy2' ) ), // bad namespace
+                               array( )
+                       ),
                        array( // #9
                                $changes['set-en-aliases'],
                                array( 'q100' => array( 'enwiki' => 'Emmy2' ) ),
-                               array( 'Emmy2' ), // or nothing, may change
-                               array(), // because no actions are to be taken, 
the effective list is empty.
+                               array(), // For the dummy page, only label and 
sitelink usage is defined.
                        ),
 
                        array( // #10
                                $changes['add-claim'],
                                array( 'q100' => array( 'enwiki' => 'Emmy2' ) ),
-                               array( 'Emmy2' )
+                               array( ) // statements are ignored
                        ),
                        array( // #11
                                $changes['remove-claim'],
                                array( 'q100' => array( 'enwiki' => 'Emmy2' ) ),
-                               array( 'Emmy2' )
+                               array( ) // statements are ignored
                        ),
 
                        array( // #12
@@ -574,8 +597,9 @@
                        ),
                        array( // #15
                                $changes['change-enwiki-sitelink'],
-                               array( 'q100' => array( 'enwiki' => 'Emmy' ) ),
-                               array( 'Emmy', 'Emmy2' )
+                               array( 'q100' => array( 'enwiki' => 'Emmy' ), 
'q200' => array( 'enwiki' => 'Emmy2' ) ),
+                               array( 'Emmy', 'Emmy2' ),
+                               true
                        ),
                        array( // #16
                                $changes['change-enwiki-sitelink-badges'],
@@ -631,7 +655,7 @@
        /**
         * @dataProvider provideGetPagesToUpdate
         */
-       public function testGetPagesToUpdate( Change $change, $entities, array 
$expected ) {
+       public function testGetPagesToUpdate( Change $change, $entities, array 
$expected, $dummy = false ) {
                $handler = $this->newChangeHandler( null, $entities );
 
                $toUpdate = $handler->getPagesToUpdate( $change );
@@ -649,9 +673,7 @@
                $cases = array();
 
                foreach ( $pto as $case ) {
-                       // $case[2] is the list of pages to update,
-                       // $case[3] may be a list filtered according to the 
actions that apply.
-                       $updated = isset( $case[3] ) ? $case[3] : $case[2];
+                       $updated = $case[2];
 
                        $cases[] = array(
                                $case[0], // $change
diff --git 
a/client/tests/phpunit/includes/DataAccess/PropertyParserFunction/RunnerTest.php
 
b/client/tests/phpunit/includes/DataAccess/PropertyParserFunction/RunnerTest.php
index 822f607..a088cef 100644
--- 
a/client/tests/phpunit/includes/DataAccess/PropertyParserFunction/RunnerTest.php
+++ 
b/client/tests/phpunit/includes/DataAccess/PropertyParserFunction/RunnerTest.php
@@ -43,15 +43,24 @@
                );
 
                $this->assertEquals( $expected, $result );
-               $this->assertUsageTracking( $itemId, EntityUsage::ALL_USAGE, 
$parser->getOutput() );
+               $this->assertUsageTracking( $itemId, EntityUsage::OTHER_USAGE, 
$parser->getOutput() );
        }
 
        private function assertUsageTracking( ItemId $id, $aspect, ParserOutput 
$parserOutput ) {
                $usageAcc = new ParserOutputUsageAccumulator( $parserOutput );
-               $usage = $usageAcc->getUsages();
+               $usages = $usageAcc->getUsages();
                $expected = new EntityUsage( $id, $aspect );
 
-               $this->assertContains( $expected, $usage, '', false, false );
+               $usageIdentities = array_map(
+                       function ( EntityUsage $usage ) {
+                               return $usage->getIdentityString();
+                       },
+                       $usages
+               );
+
+               $expectedIdentities = array( $expected->getIdentityString() );
+
+               $this->assertEquals( $expectedIdentities, array_values( 
$usageIdentities ) );
        }
 
        private function getSiteLinkLookup( ItemId $itemId ) {
diff --git a/client/tests/phpunit/includes/Usage/PageEntityUsagesTest.php 
b/client/tests/phpunit/includes/Usage/PageEntityUsagesTest.php
new file mode 100644
index 0000000..d923d26
--- /dev/null
+++ b/client/tests/phpunit/includes/Usage/PageEntityUsagesTest.php
@@ -0,0 +1,48 @@
+<?php
+namespace Wikibase\Client\Tests\Usage;
+
+use PHPUnit_Framework_TestCase;
+use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
+use Wikibase\DataModel\Entity\ItemId;
+
+/**
+ * @covers Wikibase\Client\Usage\PageEntityUsages
+ *
+ * @group Wikibase
+ * @group WikibaseClient
+ * @group WikibaseUsageTracking
+ *
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+class PageEntityUsagesTest extends PHPUnit_Framework_TestCase {
+
+       public function testGetters() {
+               $q7 = new ItemId( 'Q7' );
+               $q11 = new ItemId( 'Q11' );
+
+               $usages = array(
+                       new EntityUsage( $q7, EntityUsage::ALL_USAGE ),
+                       new EntityUsage( $q11, EntityUsage::LABEL_USAGE ),
+                       new EntityUsage( $q11, EntityUsage::TITLE_USAGE ),
+               );
+
+               $pageUsages = new PageEntityUsages( 6, $usages );
+
+               $this->assertEquals( 6, $pageUsages->getPageId() );
+
+               $expectedAspects = array(
+                       EntityUsage::LABEL_USAGE,
+                       EntityUsage::TITLE_USAGE,
+                       EntityUsage::ALL_USAGE,
+               );
+
+               $this->assertEquals( $expectedAspects, 
$pageUsages->getAspects() );
+               $this->assertEquals( array( 'Q11' => $q11, 'Q7' => $q7 ), 
$pageUsages->getEntityIds() );
+               $this->assertEquals( array( 'Q11#L', 'Q11#T', 'Q7#X' ), 
array_keys( $pageUsages->getUsages() ) );
+
+               $this->assertEquals( array( EntityUsage::ALL_USAGE ), 
$pageUsages->getUsageAspects( $q7 ) );
+       }
+
+}
diff --git 
a/client/tests/phpunit/includes/Usage/UsageAccumulatorContractTester.php 
b/client/tests/phpunit/includes/Usage/UsageAccumulatorContractTester.php
index 0f38d66..ea033eb 100644
--- a/client/tests/phpunit/includes/Usage/UsageAccumulatorContractTester.php
+++ b/client/tests/phpunit/includes/Usage/UsageAccumulatorContractTester.php
@@ -33,7 +33,7 @@
 
                $this->usageAccumulator->addSiteLinksUsage( $q2 );
                $this->usageAccumulator->addLabelUsage( $q2 );
-               $this->usageAccumulator->addPageUsage( $q2 );
+               $this->usageAccumulator->addTitleUsage( $q2 );
                $this->usageAccumulator->addAllUsage( $q3 );
 
                $usage = $this->usageAccumulator->getUsages();
diff --git a/client/tests/phpunit/includes/Usage/UsageAspectTransformerTest.php 
b/client/tests/phpunit/includes/Usage/UsageAspectTransformerTest.php
new file mode 100644
index 0000000..7d7c041
--- /dev/null
+++ b/client/tests/phpunit/includes/Usage/UsageAspectTransformerTest.php
@@ -0,0 +1,148 @@
+<?php
+namespace Wikibase\Client\Usage\Tests;
+
+use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
+use Wikibase\Client\Usage\UsageAspectTransformer;
+use Wikibase\DataModel\Entity\BasicEntityIdParser;
+use Wikibase\DataModel\Entity\ItemId;
+
+/**
+ * @covers Wikibase\Client\Usage\UsageAspectTransformer
+ *
+ * @group Wikibase
+ * @group WikibaseClient
+ * @group WikibaseUsageTracking
+ *
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+class UsageAspectTransformerTest extends \PHPUnit_Framework_TestCase {
+
+       public function testGetRelevantAspects() {
+               $q1 = new ItemId( 'Q1' );
+               $q99 = new ItemId( 'Q99' );
+
+               $aspects = array(
+                       EntityUsage::TITLE_USAGE,
+                       EntityUsage::LABEL_USAGE,
+               );
+
+               $transformer = new UsageAspectTransformer();
+               $transformer->setRelevantAspects( $q1, $aspects );
+
+               $this->assertEquals( $aspects, 
$transformer->getRelevantAspects( $q1 ) );
+               $this->assertEquals( array(), $transformer->getRelevantAspects( 
$q99 ) );
+       }
+
+       public function provideGetFilteredUsages() {
+               $q1 = new ItemId( 'Q1' );
+
+               return array(
+                       'empty' => array(
+                               $q1,
+                               array(),
+                               array(),
+                               array()
+                       ),
+                       'non relevant' => array(
+                               $q1,
+                               array(),
+                               array( 'X' ),
+                               array()
+                       ),
+                       'non used' => array(
+                               $q1,
+                               array( 'X' ),
+                               array(),
+                               array()
+                       ),
+                       'simple filter' => array(
+                               $q1,
+                               array( 'T', 'L' ),
+                               array( 'L', 'S' ),
+                               array( 'Q1#L' )
+                       ),
+                       'all filter' => array(
+                               $q1,
+                               array( 'X' ),
+                               array( 'S', 'L' ),
+                               array( 'Q1#L', 'Q1#S' )
+                       ),
+                       'filter all' => array(
+                               $q1,
+                               array( 'S', 'L' ),
+                               array( 'X' ),
+                               array( 'Q1#L', 'Q1#S' )
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideGetFilteredUsages
+        */
+       public function testGetFilteredUsages( $entityId, $relevant, $used, 
$expected ) {
+               $transformer = new UsageAspectTransformer();
+               $transformer->setRelevantAspects( $entityId, $relevant );
+
+               $usages = $transformer->getFilteredUsages( $entityId, $used );
+               $this->assertEquals( $expected, array_keys( $usages ) );
+       }
+
+       public function provideTransformPageEntityUsages() {
+               $q1 = new ItemId( 'Q1' );
+               $q2 = new ItemId( 'Q2' );
+
+               $usages = new PageEntityUsages( 23, array(
+                       new EntityUsage( $q1, EntityUsage::LABEL_USAGE ),
+                       new EntityUsage( $q1, EntityUsage::TITLE_USAGE ),
+                       new EntityUsage( $q2, EntityUsage::ALL_USAGE ),
+               ) );
+
+               return array(
+                       'empty' => array(
+                               array(),
+                               $usages,
+                               array()
+                       ),
+                       'non relevant' => array(
+                               array(),
+                               $usages,
+                               array()
+                       ),
+                       'simple filter' => array(
+                               array(
+                                       'Q1' => array( 'T' ),
+                               ),
+                               $usages,
+                               array( 'Q1#T' )
+                       ),
+                       'all filter' => array(
+                               array(
+                                       'Q2' => array( 'T', 'L' ),
+                                       'Q1' => array( 'X' ),
+                               ),
+                               $usages,
+                               array( 'Q1#L', 'Q1#T', 'Q2#L', 'Q2#T' )
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideTransformPageEntityUsages
+        */
+       public function testTransformPageEntityUsages( $relevant, 
PageEntityUsages $usages, $expected ) {
+               $transformer = new UsageAspectTransformer();
+               $idParser = new BasicEntityIdParser();
+
+               foreach ( $relevant as $id => $aspects ) {
+                       $transformer->setRelevantAspects( $idParser->parse( $id 
), $aspects );
+               }
+
+               $transformed = $transformer->transformPageEntityUsages( $usages 
);
+
+               $this->assertEquals( $usages->getPageId(), 
$transformed->getPageId() );
+               $this->assertEquals( $expected, array_keys( 
$transformed->getUsages() ) );
+       }
+
+}
diff --git a/client/tests/phpunit/includes/Usage/UsageLookupContractTester.php 
b/client/tests/phpunit/includes/Usage/UsageLookupContractTester.php
index 7791451..9b86f57 100644
--- a/client/tests/phpunit/includes/Usage/UsageLookupContractTester.php
+++ b/client/tests/phpunit/includes/Usage/UsageLookupContractTester.php
@@ -3,6 +3,7 @@
 
 use PHPUnit_Framework_Assert as Assert;
 use Wikibase\Client\Usage\EntityUsage;
+use Wikibase\Client\Usage\PageEntityUsages;
 use Wikibase\Client\Usage\UsageLookup;
 use Wikibase\Client\Usage\UsageTracker;
 use Wikibase\DataModel\Entity\ItemId;
@@ -64,28 +65,31 @@
                $q4 = new ItemId( 'Q4' );
                $q6 = new ItemId( 'Q6' );
 
-               $u3i = new EntityUsage( $q3, EntityUsage::SITELINK_USAGE );
+               $u3s = new EntityUsage( $q3, EntityUsage::SITELINK_USAGE );
                $u3l = new EntityUsage( $q3, EntityUsage::LABEL_USAGE );
                $u4l = new EntityUsage( $q4, EntityUsage::LABEL_USAGE );
+               $u4t = new EntityUsage( $q4, EntityUsage::TITLE_USAGE );
 
-               $usages = array( $u3i, $u3l, $u4l );
-
-               $this->tracker->trackUsedEntities( 23, $usages );
+               $this->tracker->trackUsedEntities( 23, array( $u3s, $u3l, $u4l 
) );
+               $this->tracker->trackUsedEntities( 42, array( $u4l, $u4t ) );
 
                Assert::assertEmpty(
                        iterator_to_array( $this->lookup->getPagesUsing( array( 
$q6 ) ) )
                );
 
-               Assert::assertEquals(
-                       array( 23 ),
+               $this->assertSamePageEntityUsages(
+                       array( 23 => new PageEntityUsages( 23, array( $u3s, 
$u3l ) ) ),
                        iterator_to_array( $this->lookup->getPagesUsing( array( 
$q3 ) ) ),
                        'Pages using Q3'
                );
 
-               Assert::assertEquals(
-                       array( 23 ),
-                       iterator_to_array( $this->lookup->getPagesUsing( array( 
$q4 ) ) ),
-                       'Pages using Q4'
+               $this->assertSamePageEntityUsages(
+                       array(
+                               23 => new PageEntityUsages( 23, array( $u3l, 
$u4l ) ),
+                               42 => new PageEntityUsages( 42, array( $u4l ) ),
+                       ),
+                       iterator_to_array( $this->lookup->getPagesUsing( array( 
$q4, $q3 ), array( EntityUsage::LABEL_USAGE ) ) ),
+                       'Pages using "label" on Q4 or Q3'
                );
 
                Assert::assertEmpty(
@@ -98,14 +102,35 @@
                        'Pages using "sitelinks" on Q4'
                );
 
-               Assert::assertCount( 1,
-                       iterator_to_array( $this->lookup->getPagesUsing( array( 
$q3, $q4 ), array( EntityUsage::LABEL_USAGE, EntityUsage::SITELINK_USAGE ) ) ),
-                       'Pages using "label" or "sitelinks" on Q3 or Q4'
+               Assert::assertCount( 2,
+                       iterator_to_array( $this->lookup->getPagesUsing( array( 
$q3, $q4 ), array( EntityUsage::TITLE_USAGE, EntityUsage::SITELINK_USAGE ) ) ),
+                       'Pages using "title" or "sitelinks" on Q3 or Q4'
                );
 
                $this->tracker->trackUsedEntities( 23, array() );
        }
 
+       /**
+        *
+        * @param PageEntityUsages[] $expected
+        * @param PageEntityUsages[] $actual
+        * @param string $message
+        */
+       private function assertSamePageEntityUsages( array $expected, array 
$actual, $message = '' ) {
+               if ( $message !== '' ) {
+                       $message .= "\n";
+               }
+
+               foreach ( $expected as $key => $expectedUsages ) {
+                       $actualUsages = $actual[$key];
+
+                       Assert::assertEquals( $expectedUsages->getPageId(), 
$actualUsages->getPageId(), $message . "[Page $key] " . 'Page ID mismatches!' );
+                       Assert::assertEquals( $expectedUsages->getUsages(), 
$actualUsages->getUsages(), $message . "[Page $key] " . 'Usages:' );
+               }
+
+               Assert::assertEmpty( array_slice( $actual, count( $expected ) 
), $message . 'Extra entries found!' );
+       }
+
        public function testGetUnusedEntities() {
                $q3 = new ItemId( 'Q3' );
                $q4 = new ItemId( 'Q4' );

-- 
To view, visit https://gerrit.wikimedia.org/r/167856
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I9ce6a2e90347f6ce8a9ce9ef4d0a083592211d23
Gerrit-PatchSet: 15
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: Multichill <[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

Reply via email to