diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4b0cdb4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + name: "PHPUnit: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" + continue-on-error: ${{ matrix.experimental }} + + strategy: + matrix: + include: + - mw: 'REL1_39' + php: '8.1' + experimental: false + - mw: 'REL1_41' + php: '8.2' + experimental: false + - mw: 'REL1_42' + php: '8.3' + experimental: false + - mw: 'REL1_43' + php: '8.3' + experimental: false + - mw: 'master' + php: '8.4' + experimental: true + + runs-on: ubuntu-latest + + + defaults: + run: + working-directory: mediawiki + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl + tools: composer + + - name: Cache MediaWiki + id: cache-mediawiki + uses: actions/cache@v3 + with: + path: | + mediawiki + !mediawiki/extensions/ + !mediawiki/vendor/ + key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4 + + - name: Cache Composer cache + uses: actions/cache@v3 + with: + path: ~/.composer/cache + key: composer-php${{ matrix.php }} + + - uses: actions/checkout@v4 + with: + path: EarlyCopy + + - name: Install MediaWiki + if: steps.cache-mediawiki.outputs.cache-hit != 'true' + working-directory: ~ + run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseConstraintViolationsExport + + - uses: actions/checkout@v4 + with: + path: mediawiki/extensions/WikibaseConstraintViolationsExport + + - run: composer update + + - name: Run update.php + run: php maintenance/update.php --quick + + - name: Run PHPUnit + run: php tests/phpunit/phpunit.php -c extensions/WikibaseConstraintViolationsExport/ diff --git a/.github/workflows/installMediaWiki.sh b/.github/workflows/installMediaWiki.sh new file mode 100644 index 0000000..08e0803 --- /dev/null +++ b/.github/workflows/installMediaWiki.sh @@ -0,0 +1,54 @@ +#! /bin/bash + +MW_BRANCH=$1 +EXTENSION_NAME=$2 + +wget "https://github.com/wikimedia/mediawiki/archive/refs/heads/$MW_BRANCH.tar.gz" -nv + +tar -zxf $MW_BRANCH.tar.gz +mv mediawiki-$MW_BRANCH mediawiki + +cd mediawiki + +composer install +php maintenance/install.php --dbtype sqlite --dbuser root --dbname mw --dbpath $(pwd) --pass AdminPassword WikiName AdminUser + +cat <<'EOT' >> LocalSettings.php +error_reporting(E_ALL & ~E_DEPRECATED); +ini_set("display_errors", "1"); +$wgShowExceptionDetails = true; +$wgShowDBErrorBacktrace = true; +$wgDevelopmentWarnings = true; +EOT + +cat <> LocalSettings.php +wfLoadExtension( 'WikibaseRepository', __DIR__ . '/extensions/Wikibase/extension-repo.json' ); +require_once __DIR__ . '/extensions/Wikibase/repo/ExampleSettings.php'; + +wfLoadExtension( 'WikibaseQualityConstraints' ); +wfLoadExtension( "$EXTENSION_NAME" ); +EOT + +cat <> composer.local.json +{ + "extra": { + "merge-plugin": { + "merge-dev": true, + "include": [ + "extensions/Wikibase/composer.json", + "extensions/$EXTENSION_NAME/composer.json" + ] + } + } +} +EOT + +cd extensions +git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/WikibaseQualityConstraints --depth=1 --branch=$MW_BRANCH --recurse-submodules -j8 + +git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/Wikibase --depth=1 --branch=$MW_BRANCH -j8 && \ + cd Wikibase && \ + git submodule set-url view/lib/wikibase-serialization https://github.com/wmde/WikibaseSerializationJavaScript.git && \ + git submodule set-url view/lib/wikibase-data-values https://github.com/wmde/DataValuesJavaScript.git && \ + git submodule set-url view/lib/wikibase-data-model https://github.com/wmde/WikibaseDataModelJavaScript.git && \ + git submodule sync && git submodule init && git submodule update --recursive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..165765a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.phpunit.result.cache diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..65a2c15 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: ci test phpunit + +ci: test +test: phpunit + +phpunit: +ifdef filter + php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist --filter $(filter) +else + php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist +endif diff --git a/maintenance/ExportConstraintViolations.php b/maintenance/ExportConstraintViolations.php index 1878728..66e70df 100644 --- a/maintenance/ExportConstraintViolations.php +++ b/maintenance/ExportConstraintViolations.php @@ -4,16 +4,17 @@ namespace ProfessionalWiki\WikibaseConstraintViolationsExport\Maintenance; -use MediaWiki\Language\Language; -use MediaWiki\Maintenance\Maintenance; +use Language; +use Maintenance; use MediaWiki\MediaWikiServices; -use MediaWiki\Message\Message; +use Message; use MessageLocalizer; use ProfessionalWiki\WikibaseConstraintViolationsExport\Presentation\PlainTextViolationMessageRenderer; use ValueFormatters\FormatterOptions; use ValueFormatters\ValueFormatter; use Wikibase\DataModel\Services\EntityId\EntityIdPager; use Wikibase\Lib\Formatters\SnakFormatter; +use Wikibase\Repo\EntityIdLabelFormatterFactory; use Wikibase\Repo\Store\Sql\SqlEntityIdPagerFactory; use Wikibase\Repo\WikibaseRepo; use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult; @@ -89,7 +90,7 @@ private function getViolationMessageRenderer(): PlainTextViolationMessageRendere $language = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' ); return new PlainTextViolationMessageRenderer( - entityIdFormatter: WikibaseRepo::getEntityIdLabelFormatterFactory()->getEntityIdFormatter( $language ), + entityIdFormatter: $this->getEntityIdLabelFormatter()->getEntityIdFormatter( $language ), dataValueFormatter: $this->getValueFormatter( $language ), languageNameUtils: MediaWikiServices::getInstance()->getLanguageNameUtils(), userLanguageCode: $language->getCode(), @@ -99,6 +100,14 @@ private function getViolationMessageRenderer(): PlainTextViolationMessageRendere ); } + private function getEntityIdLabelFormatter(): EntityIdLabelFormatterFactory { + if ( method_exists( WikibaseRepo::class, 'getEntityIdLabelFormatterFactory' ) ) { + return WikibaseRepo::getEntityIdLabelFormatterFactory(); + } + + return new EntityIdLabelFormatterFactory(); + } + private function getValueFormatter( Language $language ): ValueFormatter { $formatterOptions = new FormatterOptions(); $formatterOptions->setOption( SnakFormatter::OPT_LANG, $language->getCode() ); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..e211081 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,12 @@ + + + + tests/ + + + + + src + + + diff --git a/src/Presentation/PlainTextViolationMessageRenderer.php b/src/Presentation/PlainTextViolationMessageRenderer.php index bbcd89a..014f966 100644 --- a/src/Presentation/PlainTextViolationMessageRenderer.php +++ b/src/Presentation/PlainTextViolationMessageRenderer.php @@ -4,10 +4,11 @@ namespace ProfessionalWiki\WikibaseConstraintViolationsExport\Presentation; +use Config; use DataValues\DataValue; -use MediaWiki\Config\Config; +use InvalidArgumentException; use MediaWiki\Languages\LanguageNameUtils; -use MediaWiki\Message\Message; +use Message; use MessageLocalizer; use ValueFormatters\ValueFormatter; use Wikibase\DataModel\Entity\EntityId; diff --git a/tests/Maintenance/ExportConstraintViolationsTest.php b/tests/Maintenance/ExportConstraintViolationsTest.php new file mode 100644 index 0000000..83d674f --- /dev/null +++ b/tests/Maintenance/ExportConstraintViolationsTest.php @@ -0,0 +1,173 @@ +overrideConfigValues( [ + 'WBQualityConstraintsMultiValueConstraintId' => self::MULTI_VALUE_CONSTRAINT_TYPE_ID, + 'WBQualityConstraintsSingleValueConstraintId' => self::SINGLE_VALUE_CONSTRAINT_TYPE_ID + ] ); + } + + protected function getMaintenanceClass(): string { + return ExportConstraintViolations::class; + } + + public function addDBDataOnce() { + $this->saveProperty( self::MULTI_VALUE_PROPERTY_ID, 'string', 'Multiple Values' ); + $this->saveProperty( self::SINGLE_VALUE_PROPERTY_ID, 'string', 'Single Value' ); + + $this->db->insert( + 'wbqc_constraints', + [ + [ + 'constraint_guid' => self::MULTI_VALUE_CONSTRAINT_ID, + 'pid' => ( new NumericPropertyId( self::MULTI_VALUE_PROPERTY_ID ) )->getNumericId(), + 'constraint_type_qid' => self::MULTI_VALUE_CONSTRAINT_TYPE_ID, + 'constraint_parameters' => '{}', + ], + [ + 'constraint_guid' => self::SINGLE_VALUE_CONSTRAINT_ID, + 'pid' => ( new NumericPropertyId( self::SINGLE_VALUE_PROPERTY_ID ) )->getNumericId(), + 'constraint_type_qid' => self::SINGLE_VALUE_CONSTRAINT_TYPE_ID, + 'constraint_parameters' => '{}', + ] + ] + ); + } + + public function testExportsEmptyArrayIfThereAreNoViolations() : void{ + $item1 = new Item( new ItemId( 'Q100' ) ); + $this->addStatement( $item1, self::MULTI_VALUE_PROPERTY_ID, 'multi value 1' ); + $this->addStatement( $item1, self::MULTI_VALUE_PROPERTY_ID, 'multi value 2' ); + $this->addStatement( $item1, self::SINGLE_VALUE_PROPERTY_ID, 'single value 1' ); + + $this->maintenance->execute(); + + $this->expectOutputString( '[]' ); + } + + public function testExportsViolations() : void{ + $item1 = new Item( new ItemId( 'Q100' ) ); + $this->addStatement( $item1, self::MULTI_VALUE_PROPERTY_ID, 'multi value 1' ); + $this->addStatement( $item1, self::SINGLE_VALUE_PROPERTY_ID, 'single value 1' ); + + $item2 = new Item( new ItemId( 'Q200' ) ); + $this->addStatement( $item2, self::MULTI_VALUE_PROPERTY_ID, 'multi value 1' ); + $this->addStatement( $item2, self::MULTI_VALUE_PROPERTY_ID, 'multi value 2' ); + $this->addStatement( $item2, self::SINGLE_VALUE_PROPERTY_ID, 'single value 1' ); + $this->addStatement( $item2, self::SINGLE_VALUE_PROPERTY_ID, 'single value 2' ); + + $this->maintenance->execute(); + + $this->assertEqualsCanonicalizing( + [ + 'Q100' => [ + [ + 'status' => 'warning', + 'propertyId' => self::MULTI_VALUE_PROPERTY_ID, + 'messageKey' => 'wbqc-violation-message-multi-value', + 'message' => 'This property should contain multiple values.', + 'constraintId' => self::MULTI_VALUE_CONSTRAINT_ID, + 'constraintType' => self::MULTI_VALUE_CONSTRAINT_TYPE_ID, + 'value' => 'multi value 1' + ] + ], + 'Q200' => [ + [ + 'status' => 'warning', + 'propertyId' => self::SINGLE_VALUE_PROPERTY_ID, + 'messageKey' => 'wbqc-violation-message-single-value', + 'message' => 'This property should only contain a single value.', + 'constraintId' => self::SINGLE_VALUE_CONSTRAINT_ID, + 'constraintType' => self::SINGLE_VALUE_CONSTRAINT_TYPE_ID, + 'value' => 'single value 2' + ], + [ + 'status' => 'warning', + 'propertyId' => self::SINGLE_VALUE_PROPERTY_ID, + 'messageKey' => 'wbqc-violation-message-single-value', + 'message' => 'This property should only contain a single value.', + 'constraintId' => self::SINGLE_VALUE_CONSTRAINT_ID, + 'constraintType' => self::SINGLE_VALUE_CONSTRAINT_TYPE_ID, + 'value' => 'single value 1' + ] + ] + ], + json_decode( $this->getActualOutput(), true ) + ); + } + + protected function saveEntity( EntityDocument $entity ): void { + WikibaseRepo::getEntityStore()->saveEntity( + entity: $entity, + summary: __CLASS__, + user: self::getTestSysop()->getUser() + ); + } + + protected function saveProperty( string $pId, string $type, string $label ): void { + $this->saveEntity( + new Property( + id: new NumericPropertyId( $pId ), + fingerprint: new Fingerprint( labels: new TermList( [ + new Term( languageCode: 'en', text: $label ) + ] ) ), + dataTypeId: $type + ) + ); + } + + protected function addStatement( EntityDocument $entity, string $propertyId, string $value ): void { + $statementGuidGenerator = new GuidGenerator(); + + $dataValue = new StringValue( $value ); + $snak = new PropertyValueSnak( new NumericPropertyId( $propertyId ), $dataValue ); + $statement = new Statement( $snak ); + $statementGuid = $statementGuidGenerator->newGuid( $entity->getId() ); + $statement->setGuid( $statementGuid ); + + $entity->getStatements()->addStatement( $statement ); + + $this->saveEntity( $entity ); + } + +}