jenkins-bot has submitted this change and it was merged. Change subject: Introducing changes subscription infrastructure. ......................................................................
Introducing changes subscription infrastructure. This introduces the wb_changes_subscriptions table for tracking which clients (subscribers) are interested in changes to which entities. DEPLOY: The 'useLegacyChangesSubscription' setting should be set to true on the repo AND all client wikis, until the wb_changes_subscription table on the repo exists and is populated. Bug: T86184 Change-Id: I120ed3bc4a68f5b6791b176e1e714deba8861b24 --- M client/config/WikibaseClient.default.php M docs/options.wiki M repo/Wikibase.php M repo/config/Wikibase.default.php A repo/includes/store/sql/ChangesSubscriptionSchemaUpdater.php A repo/includes/store/sql/ChangesSubscriptionTableBuilder.php A repo/maintenance/populateChangesSubscription.php A repo/sql/changes_subscription.sql A repo/tests/phpunit/includes/store/sql/ChangesSubscriptionTableBuilderTest.php 9 files changed, 635 insertions(+), 1 deletion(-) Approvals: JanZerebecki: Looks good to me, approved jenkins-bot: Verified diff --git a/client/config/WikibaseClient.default.php b/client/config/WikibaseClient.default.php index 501160a..756693f 100644 --- a/client/config/WikibaseClient.default.php +++ b/client/config/WikibaseClient.default.php @@ -48,6 +48,9 @@ // Enable in case wbc_entity_usage does not exist or is not yet populated. 'useLegacyUsageIndex' => false, + // Enable in case wb_changes_subscription does not exist (on the repo) or is not yet populated. + 'useLegacyChangesSubscription' => false, + /** * @todo this is a bit wikimedia-specific and need to find a better place for this stuff, * such as mediawiki-config, mediawiki messages for custom orders, or somewhere. diff --git a/docs/options.wiki b/docs/options.wiki index e8c3a6d..593d091 100644 --- a/docs/options.wiki +++ b/docs/options.wiki @@ -62,6 +62,7 @@ ;useRedirectTargetColumn: Whether to use the epp_redirect_target column in the wb_entity_per_page table for detecting redirects. True per default, can be set to false in an environment where the necessary database update can't be deployed right away. To set up the table manually, run repo/sql/AddEppRedirectTarget.sql to create it, then use repo/maintenance/rebuildEntityPerPage.php to rebuild the table if neccessary. ;internalEntitySerializerClass: The class name of a serializer that is to be used when serializing entities for storage. Defaults to null, causing the default entity serializer from the SerializerFactory to be used. Should be set to Wikibase\Lib\Serializers\LegacyInternalEntitySerializer for compatibility with client wikis that run older code. ;transformLegacyFormatOnExport: Whether entity revisions stored in a legacy format should be converted on the fly while exporting. Enabled per default, if internalEntitySerializerClass is not set, and disable by default if internalEntitySerializerClass is set. Must be disabled if internalEntitySerializerClass is set to the legacy serializer. +;useLegacyChangesSubscription: whether the sitelinks table should be used to emulate notification subscriptions. Should be set to true if the approproate database schema change has not been deployed yet. == Client Settings == @@ -110,4 +111,5 @@ :;<code>'CachingSqlStore'</code>: No direct access to the repo's database. Use cache information received from change notifications. '''Warning:''' dies is currently dysfunctional. :;<code>null</code>: Automatic, use <code>'DirectSqlStore'</code> if <code>repoDatabase</code> is set, <code>'CachingSqlStore'</code> if not. : '''Caveat:''' should always be <code>null</code> at the moment, because some code relies on the <code>repoDatabase</code> setting to determine whether the local cache tables should be used. -;useLegacyUsageIndex: whether the sitelinks table should be used to emulate usage tracking. May be set to true if the approproate database schema change has not been deployed yet. +;useLegacyUsageIndex: whether the sitelinks table should be used to emulate usage tracking. Should be set to true if the approproate database schema change has not been deployed yet. +;useLegacyChangesSubscription: whether the sitelinks table should be used to emulate notification subscriptions. Should be set to true if the approproate database schema change has not been deployed to the repository database yet. diff --git a/repo/Wikibase.php b/repo/Wikibase.php index 9be6173..1b1fd4c 100644 --- a/repo/Wikibase.php +++ b/repo/Wikibase.php @@ -201,6 +201,9 @@ $wgHooks['BaseTemplateToolbox'][] = 'Wikibase\RepoHooks::onBaseTemplateToolbox'; $wgHooks['SkinTemplateBuildNavUrlsNav_urlsAfterPermalink'][] = 'Wikibase\RepoHooks::onSkinTemplateBuildNavUrlsNav_urlsAfterPermalink'; + // update hooks + $wgHooks['LoadExtensionSchemaUpdates'][] = '\Wikibase\Repo\Store\Sql\ChangesSubscriptionSchemaUpdater::onSchemaUpdate'; + // Resource Loader Modules: $wgResourceModules = array_merge( $wgResourceModules, include( __DIR__ . "/resources/Resources.php" ) ); diff --git a/repo/config/Wikibase.default.php b/repo/config/Wikibase.default.php index a83d0b2..a2be72c 100644 --- a/repo/config/Wikibase.default.php +++ b/repo/config/Wikibase.default.php @@ -102,6 +102,8 @@ return $uri; }, + // Enable in case wb_changes_subscription does not exist or is not yet populated. + 'useLegacyChangesSubscription' => false, ); return $defaults; diff --git a/repo/includes/store/sql/ChangesSubscriptionSchemaUpdater.php b/repo/includes/store/sql/ChangesSubscriptionSchemaUpdater.php new file mode 100644 index 0000000..cf4aabb --- /dev/null +++ b/repo/includes/store/sql/ChangesSubscriptionSchemaUpdater.php @@ -0,0 +1,107 @@ +<?php + +namespace Wikibase\Repo\Store\Sql; + +use DatabaseUpdater; +use Wikibase\Lib\Reporting\ObservableMessageReporter; +use Wikibase\Repo\WikibaseRepo; + +/** + * Schema updater for the wb_changes_subscription table. + * + * @license GPL 2+ + * @author Daniel Kinzler + */ +class ChangesSubscriptionSchemaUpdater { + + /** + * @var DatabaseUpdater + */ + private $dbUpdater; + + /** + * @param DatabaseUpdater $dbUpdater + */ + public function __construct( DatabaseUpdater $dbUpdater ) { + $this->dbUpdater = $dbUpdater; + } + + /** + * Static entry point for MediaWiki's LoadExtensionSchemaUpdates hook. + * + * @param DatabaseUpdater $dbUpdater + * + * @return bool + */ + public static function onSchemaUpdate( DatabaseUpdater $dbUpdater ) { + if ( WikibaseRepo::getDefaultInstance()->getSettings()->getSetting( 'useLegacyChangesSubscription' ) ) { + return true; + } + + $changesSubscriptionSchemaUpdater = new self( $dbUpdater ); + $changesSubscriptionSchemaUpdater->doSchemaUpdate(); + + return true; + } + + /** + * Applies any schema updates + */ + public function doSchemaUpdate() { + $table = 'wb_changes_subscription'; + + if ( !$this->dbUpdater->tableExists( $table ) ) { + $db = $this->dbUpdater->getDB(); + $script = $this->getUpdateScriptPath( 'changes_subscription', $db->getType() ); + $this->dbUpdater->addExtensionTable( $table, $script ); + + // Register function for populating the table. + // Note that this must be done with a static function, + // for reasons that do not need explaining at this juncture. + $this->dbUpdater->addExtensionUpdate( array( + array( __CLASS__, 'fillSubscriptionTable' ), + $table + ) ); + } + } + + /** + * Static wrapper for EntityUsageTableBuilder::fillUsageTable + * + * @param DatabaseUpdater $dbUpdater + * @param string $table + */ + public static function fillSubscriptionTable( DatabaseUpdater $dbUpdater, $table ) { + $primer = new ChangesSubscriptionTableBuilder( + wfGetLB(), // would be nice to pass in $dbUpdater->getDB(). + $table, + 1000 + ); + + $reporter = new ObservableMessageReporter(); + $reporter->registerReporterCallback( function( $msg ) use ( $dbUpdater ) { + $dbUpdater->output( "\t$msg\n" ); + } ); + $primer->setProgressReporter( $reporter ); + + $primer->fillSubscriptionTable(); + } + + private function getUpdateScriptPath( $name, $type ) { + $extensions = array( + '.sql', + '.' . $type . '.sql', + ); + + foreach ( $extensions as $ext ) { + $path = __DIR__ . '/../../../sql/' . $name . $ext; + + if ( file_exists( $path ) ) { + return $path; + } + } + + throw new \MWException( "Could not find schema update script '$name'." ); + } + +} diff --git a/repo/includes/store/sql/ChangesSubscriptionTableBuilder.php b/repo/includes/store/sql/ChangesSubscriptionTableBuilder.php new file mode 100644 index 0000000..947f7d8 --- /dev/null +++ b/repo/includes/store/sql/ChangesSubscriptionTableBuilder.php @@ -0,0 +1,249 @@ +<?php + +namespace Wikibase\Repo\Store\Sql; + +use DatabaseBase; +use InvalidArgumentException; +use LoadBalancer; +use ResultWrapper; +use Wikibase\DataModel\Entity\EntityId; +use Wikibase\DataModel\Entity\ItemId; +use Wikibase\Lib\Reporting\ExceptionHandler; +use Wikibase\Lib\Reporting\LogWarningExceptionHandler; +use Wikibase\Lib\Reporting\MessageReporter; +use Wikibase\Lib\Reporting\NullMessageReporter; + +/** + * Implements initial population (priming) for the wb_changes_subscription table, + * based on the wb_items_per_site. Any wiki linked via the wb_items_per_site table + * will be considered a subscriber. + * + * @license GPL 2+ + * @author Daniel Kinzler + */ +class ChangesSubscriptionTableBuilder { + + /** + * @var LoadBalancer + */ + private $loadBalancer; + + /** + * @var int + */ + private $batchSize; + + /** + * @var ExceptionHandler + */ + private $exceptionHandler; + + /** + * @var MessageReporter + */ + private $progressReporter; + + /** + * @param LoadBalancer $loadBalancer + * @param string $tableName + * @param int $batchSize + * + * @throws InvalidArgumentException + */ + public function __construct( LoadBalancer $loadBalancer, $tableName, $batchSize = 1000 ) { + if ( !is_string( $tableName ) ) { + throw new InvalidArgumentException( '$tableName must be a string' ); + } + + if ( !is_int( $batchSize ) || $batchSize < 1 ) { + throw new InvalidArgumentException( '$batchSize must be an integer >= 1' ); + } + + $this->loadBalancer = $loadBalancer; + $this->tableName = $tableName; + $this->batchSize = $batchSize; + + $this->exceptionHandler = new LogWarningExceptionHandler(); + $this->progressReporter = new NullMessageReporter(); + } + + /** + * @param MessageReporter $progressReporter + */ + public function setProgressReporter( MessageReporter $progressReporter ) { + $this->progressReporter = $progressReporter; + } + + /** + * @return MessageReporter + */ + public function getProgressReporter() { + return $this->progressReporter; + } + + /** + * @param ExceptionHandler $exceptionHandler + */ + public function setExceptionHandler( ExceptionHandler $exceptionHandler ) { + $this->exceptionHandler = $exceptionHandler; + } + + /** + * @return ExceptionHandler + */ + public function getExceptionHandler() { + return $this->exceptionHandler; + } + + /** + * Fill the subscription table with rows based on entries in page_props. + * + * @param ItemId $startItem The item to start with. + */ + public function fillSubscriptionTable( ItemId $startItem = null ) { + $continuation = $startItem === null ? null : array( $startItem->getNumericId(), '' ); + + while ( true ) { + $count = $this->processSubscriptionBatch( $continuation ); + + if ( $count > 0 ) { + $this->progressReporter->reportMessage( "Populating subscription table: inserted $count subscriptions, continuing at item #{$continuation[0]}." ); + } else { + break; + } + }; + } + + /** + * @param array &$continuation + * + * @return int The number of subscriptions inserted. + */ + private function processSubscriptionBatch( &$continuation = array() ) { + $db = $this->loadBalancer->getConnection( DB_MASTER ); + + $entityPerPage = $this->getSubscriptionBatch( $db, $continuation ); + + if ( empty( $entityPerPage ) ) { + return 0; + } + + $count = $this->insertSubscriptionBatch( $db, $entityPerPage ); + + $this->loadBalancer->reuseConnection( $db ); + + return $count; + } + + /** + * @param DatabaseBase $db + * @param array[] $subscriptionsPerItem + * + * @return int The number of rows inserted. + */ + private function insertSubscriptionBatch( DatabaseBase $db, array $subscriptionsPerItem ) { + $db->startAtomic( __METHOD__ ); + + $c = 0; + foreach ( $subscriptionsPerItem as $itemId => $subscribers ) { + $rows = $this->makeSubscriptionRows( $itemId, $subscribers ); + + $db->insert( + $this->tableName, + $rows, + __METHOD__, + array( + 'IGNORE' + ) + ); + + $c+= count( $rows ); + } + + $db->endAtomic( __METHOD__ ); + return $c; + } + + /** + * @param DatabaseBase $db + * @param array &$continuation + * + * @return array[] An associative array mapping item IDs to lists of site IDs. + */ + private function getSubscriptionBatch( DatabaseBase $db, &$continuation = array() ) { + + if ( empty( $continuation ) ) { + $continuationCondition = '1'; + } else { + list( $fromItemId, $fromSiteId ) = $continuation; + $continuationCondition = 'ips_item_id > ' . (int)$fromItemId + . ' OR ( ' + . 'ips_item_id = ' . (int)$fromItemId + . ' AND ' + . 'ips_site_id > ' . $db->addQuotes( $fromSiteId ) + . ' )'; + } + + $res = $db->select( + 'wb_items_per_site', + array( 'ips_item_id', 'ips_site_id' ), + $continuationCondition, + __METHOD__, + array( + 'LIMIT' => $this->batchSize, + 'ORDER BY ips_item_id, ips_site_id' + ) + ); + + return $this->slurpSubscriptions( $res, $continuation ); + } + + /** + * @param ResultWrapper $res A result set with the ips_item_id and ips_site_id fields + * set for each row. + * @param array &$continuation + * + * @return array[] An associative array mapping item IDs to lists of site IDs. + */ + private function slurpSubscriptions( ResultWrapper $res, &$continuation = array() ) { + $entityPerPage = array(); + + $currentId = 0; + $key = null; + + foreach ( $res as $row ) { + if ( $row->ips_item_id != $currentId ) { + $currentId = $row->ips_item_id; + $key = ItemId::newFromNumber( $currentId )->getSerialization(); + } + + $entityPerPage[$key][] = $row->ips_site_id; + $continuation = array( $currentId, $row->ips_site_id ); + } + + return $entityPerPage; + } + + /** + * Returns a list of rows for insertion, using DatabaseBase's multi-row insert mechanism. + * Each row is represented as array( $itemId, $subscriber ). + * + * @param string $itemId + * @param string[] $subscribers + * + * @return array[] rows + */ + private function makeSubscriptionRows( $itemId, array $subscribers ) { + $rows = array(); + + foreach ( $subscribers as $subscriber ) { + $rows[] = array( + 'cs_entity_id' => $itemId, + 'cs_subscriber_id' => $subscriber + ); + } + + return $rows; + } + +} diff --git a/repo/maintenance/populateChangesSubscription.php b/repo/maintenance/populateChangesSubscription.php new file mode 100644 index 0000000..fc8818a --- /dev/null +++ b/repo/maintenance/populateChangesSubscription.php @@ -0,0 +1,98 @@ +<?php + +namespace Wikibase; + +use LoggedUpdateMaintenance; +use Wikibase\DataModel\Entity\EntityIdParsingException; +use Wikibase\DataModel\Entity\ItemId; +use Wikibase\Repo\Store\Sql\ChangesSubscriptionTableBuilder; +use Wikibase\Repo\WikibaseRepo; +use Wikibase\Lib\Reporting\ObservableMessageReporter; +use Wikibase\Lib\Reporting\ReportingExceptionHandler; + +$basePath = getenv( 'MW_INSTALL_PATH' ) !== false ? getenv( 'MW_INSTALL_PATH' ) : __DIR__ . '/../../../..'; + +require_once $basePath . '/maintenance/Maintenance.php'; + +/** + * Maintenance script for populating wb_changes_subscription based on the page_props table. + * + * @since 0.4 + * + * @licence GNU GPL v2+ + * @author Daniel Kinzler + */ +class PopulateChangesSubscription extends LoggedUpdateMaintenance { + + public function __construct() { + $this->mDescription = 'Populate the wb_changes_subscription table based on entries in page_props.'; + + $this->addOption( 'start-item', "The page ID to start from.", false, true ); + + parent::__construct(); + + $this->setBatchSize( 1000 ); + } + + /** + * @see LoggedUpdateMaintenance::doDBUpdates + * + * @return boolean + */ + public function doDBUpdates() { + if ( !defined( 'WB_VERSION' ) ) { + $this->output( "You need to have Wikibase enabled in order to use this maintenance script!\n\n" ); + exit; + } + + $idParser = WikibaseRepo::getDefaultInstance()->getEntityIdParser(); + $startItemOption = $this->getOption( 'start-item' ); + + $startItem = $startItemOption === null ? null : $idParser->parse( $startItemOption ); + + if ( $startItem !== null && !( $startItem instanceof ItemId ) ) { + throw new EntityIdParsingException( 'Not an Item ID: ' . $startItemOption ); + } + + $reporter = new ObservableMessageReporter(); + $reporter->registerReporterCallback( + array( $this, 'report' ) + ); + + $builder = new ChangesSubscriptionTableBuilder( + wfGetLB(), + 'wb_changes_subscription', + $this->mBatchSize + ); + + $builder->setProgressReporter( $reporter ); + $builder->setExceptionHandler( new ReportingExceptionHandler( $reporter ) ); + + $builder->fillSubscriptionTable( $startItem ); + return true; + } + + /** + * @see LoggedUpdateMaintenance::getUpdateKey + * + * @return string + */ + public function getUpdateKey() { + return 'Wikibase\PopulateChangesSubscription'; + } + + /** + * Outputs a message vis the output() method. + * + * @since 0.4 + * + * @param $msg + */ + public function report( $msg ) { + $this->output( "$msg\n" ); + } + +} + +$maintClass = 'Wikibase\PopulateChangesSubscription'; +require_once( RUN_MAINTENANCE_IF_MAIN ); diff --git a/repo/sql/changes_subscription.sql b/repo/sql/changes_subscription.sql new file mode 100644 index 0000000..1795a69 --- /dev/null +++ b/repo/sql/changes_subscription.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS /*_*/wb_changes_subscription ( + cs_row_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, + cs_entity_id VARBINARY(255) NOT NULL, -- the ID of the entity subscribed to + cs_subscriber_id VARBINARY(255) NOT NULL -- the ID of the subscriber (e.g. a domain name or database name) +) /*$wgDBTableOptions*/; + +-- look up a subscription, or all subscribers of an entity +CREATE UNIQUE INDEX /*i*/cs_entity_id ON /*_*/wb_changes_subscription ( cs_entity_id, cs_subscriber_id ); + +-- look up all subscriptions of a subscriber +CREATE INDEX /*i*/cs_subscriber_id ON /*_*/wb_changes_subscription ( cs_subscriber_id ) ; diff --git a/repo/tests/phpunit/includes/store/sql/ChangesSubscriptionTableBuilderTest.php b/repo/tests/phpunit/includes/store/sql/ChangesSubscriptionTableBuilderTest.php new file mode 100644 index 0000000..e4e7990 --- /dev/null +++ b/repo/tests/phpunit/includes/store/sql/ChangesSubscriptionTableBuilderTest.php @@ -0,0 +1,159 @@ +<?php + +namespace Wikibase\Repo\Tests\Store\Sql; + +use PHPUnit_Framework_MockObject_Matcher_Invocation; +use Wikibase\DataModel\Entity\ItemId; +use Wikibase\Lib\Reporting\ExceptionHandler; +use Wikibase\Lib\Reporting\MessageReporter; +use Wikibase\Repo\Store\Sql\ChangesSubscriptionTableBuilder; +use Wikibase\Repo\WikibaseRepo; + +/** + * @covers Wikibase\Repo\Store\Sql\ChangesSubscriptionTableBuilder + * + * @group Wikibase + * @group WikibaseRepo + * @group WikibaseUsageTracking + * @group Database + * + * @license GPL 2+ + * @author Daniel Kinzler + */ +class ChangesSubscriptionTableBuilderTest extends \MediaWikiTestCase { + + const TABLE_NAME = 'wb_changes_subscription'; + + public function setUp() { + if ( WikibaseRepo::getDefaultInstance()->getSettings()->getSetting( 'useLegacyChangesSubscription' ) ) { + $this->markTestSkipped( 'Skipping test for ChangesSubscriptionTableBuilder, because the useLegacyChangesSubscription option is set.' ); + } + + $this->tablesUsed[] = self::TABLE_NAME; + $this->tablesUsed[] = 'wb_items_per_site'; + + parent::setUp(); + } + + private function getChangesSubscriptionTableBuilder( $batchSize = 10 ) { + $loadBalancer = wfGetLB(); + + return new ChangesSubscriptionTableBuilder( $loadBalancer, self::TABLE_NAME, $batchSize ); + } + + public function testFillSubscriptionTable() { + $this->truncateItemPerSite(); + $this->putItemPerSite( array( + array( 11, 'dewiki' ), + array( 11, 'enwiki' ), + array( 22, 'dewiki' ), + array( 22, 'frwiki' ), + ) ); + + $primer = $this->getChangesSubscriptionTableBuilder( 3 ); + $primer->setProgressReporter( $this->getMessageReporter( $this->exactly( 2 ) ) ); + $primer->setExceptionHandler( $this->getExceptionHandler( $this->exactly( 0 ) ) ); + + $primer->fillSubscriptionTable(); + + $actual = $this->fetchAllSubscriptions(); + sort( $actual ); + + $expected = array( + 'dewiki@Q11', + 'dewiki@Q22', + 'enwiki@Q11', + 'frwiki@Q22', + ); + + $this->assertEquals( $expected, $actual ); + } + + public function testFillSubscriptionTable_startItem() { + $this->truncateItemPerSite(); + $this->putItemPerSite( array( + array( 11, 'dewiki' ), + array( 11, 'enwiki' ), + array( 22, 'dewiki' ), + array( 22, 'frwiki' ), + ) ); + + $primer = $this->getChangesSubscriptionTableBuilder( 3 ); + $primer->setProgressReporter( $this->getMessageReporter( $this->exactly( 1 ) ) ); + $primer->setExceptionHandler( $this->getExceptionHandler( $this->exactly( 0 ) ) ); + + $primer->fillSubscriptionTable( new ItemId( 'Q20' ) ); + + $actual = $this->fetchAllSubscriptions(); + sort( $actual ); + + $expected = array( + 'dewiki@Q22', + 'frwiki@Q22', + ); + + $this->assertEquals( $expected, $actual ); + } + + private function truncateItemPerSite() { + $db = wfGetDB( DB_MASTER ); + $db->delete( 'wb_items_per_site', '*' ); + } + + private function putItemPerSite( array $entries ) { + $db = wfGetDB( DB_MASTER ); + + $db->startAtomic( __METHOD__ ); + + foreach ( $entries as $entry ) { + list( $itemId, $siteId ) = $entry; + $db->insert( 'wb_items_per_site', array( + 'ips_item_id' => (int)$itemId, + 'ips_site_id' => $siteId, + 'ips_site_page' => 'Page_about_Q' . $itemId. '_on_' . $siteId, + ), __METHOD__ ); + } + + $db->endAtomic( __METHOD__ ); + } + + private function fetchAllSubscriptions() { + $db = wfGetDB( DB_MASTER ); + + $res = $db->select( self::TABLE_NAME, "*", '', __METHOD__ ); + + $subscriptions = array(); + foreach ( $res as $row ) { + $subscriptions[] = $row->cs_subscriber_id . '@' . $row->cs_entity_id; + } + + return $subscriptions; + } + + /** + * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher + * + * @return ExceptionHandler + */ + private function getExceptionHandler( PHPUnit_Framework_MockObject_Matcher_Invocation $matcher ) { + $mock = $this->getMock( 'Wikibase\Lib\Reporting\ExceptionHandler' ); + $mock->expects( $matcher ) + ->method( 'handleException' ); + + return $mock; + } + + /** + * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher + * + * @return MessageReporter + */ + private function getMessageReporter( PHPUnit_Framework_MockObject_Matcher_Invocation $matcher ) { + $mock = $this->getMock( 'Wikibase\Lib\Reporting\MessageReporter' ); + $mock->expects( $matcher ) + ->method( 'reportMessage' ); + + return $mock; + } + +} -- To view, visit https://gerrit.wikimedia.org/r/183839 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I120ed3bc4a68f5b6791b176e1e714deba8861b24 Gerrit-PatchSet: 7 Gerrit-Project: mediawiki/extensions/Wikibase Gerrit-Branch: master Gerrit-Owner: Daniel Kinzler <daniel.kinz...@wikimedia.de> Gerrit-Reviewer: Adrian Lang <adrian.he...@wikimedia.de> Gerrit-Reviewer: Aude <aude.w...@gmail.com> Gerrit-Reviewer: Daniel Kinzler <daniel.kinz...@wikimedia.de> Gerrit-Reviewer: JanZerebecki <jan.wikime...@zerebecki.de> Gerrit-Reviewer: Jeroen De Dauw <jeroended...@gmail.com> Gerrit-Reviewer: Springle <sprin...@wikimedia.org> Gerrit-Reviewer: Thiemo Mättig (WMDE) <thiemo.maet...@wikimedia.de> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits