jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/345302 )
Change subject: Check Constraints with SPARQL
......................................................................
Check Constraints with SPARQL
Two new checkers are added, which check Type and Value type constraints
using SPARQL, and UniqueValueChecker is implemented, also using SPARQL.
They use a new helper class, SparqlHelper, which translates requests
into SPARQL queries and executes them.
ConstraintReportFactory associates the new checkers with the constraint
types “Type (SPARQL)” and “Value type (SPARQL)”.
A new configuration variable for the SPARQL endpoint to be used is
added, defaulting to the Wikidata Query Service.
Change-Id: I8af2e92215c026805df64babc5baf2a4ac8bc25e
---
M api/CheckConstraints.php
M extension.json
M i18n/en.json
M i18n/qqq.json
A includes/ConstraintCheck/Checker/TypeSparqlChecker.php
M includes/ConstraintCheck/Checker/UniqueValueChecker.php
A includes/ConstraintCheck/Checker/ValueTypeSparqlChecker.php
A includes/ConstraintCheck/Helper/SparqlHelper.php
A includes/ConstraintCheck/Helper/SparqlHelperException.php
M includes/ConstraintReportFactory.php
A tests/phpunit/Checker/TypeChecker/Q42.json
A tests/phpunit/Checker/TypeChecker/TypeSparqlCheckerTest.php
A tests/phpunit/Checker/TypeChecker/ValueTypeSparqlCheckerTest.php
M tests/phpunit/Checker/ValueCountChecker/UniqueValueCheckerTest.php
M tests/phpunit/DefaultConfig.php
M tests/phpunit/DelegatingConstraintCheckerTest.php
A tests/phpunit/Helper/SparqlHelperTest.php
A tests/phpunit/SparqlHelperMock.php
18 files changed, 1,110 insertions(+), 27 deletions(-)
Approvals:
Jonas Kress (WMDE): Looks good to me, approved
jenkins-bot: Verified
diff --git a/api/CheckConstraints.php b/api/CheckConstraints.php
index e18fbb8..c5c9d8e 100644
--- a/api/CheckConstraints.php
+++ b/api/CheckConstraints.php
@@ -116,7 +116,9 @@
$repo->getEntityLookup(),
$statementGuidParser,
MediaWikiServices::getInstance()->getMainConfig(),
- $constraintParameterRenderer
+ $constraintParameterRenderer,
+ $repo->getRdfVocabulary(),
+ $repo->getEntityIdParser()
);
return new CheckConstraints( $main, $name, $prefix,
$repo->getEntityIdParser(),
diff --git a/extension.json b/extension.json
index 176a2f7..ce0160a 100644
--- a/extension.json
+++ b/extension.json
@@ -66,6 +66,11 @@
"description": "Whether to import property constraint
statements into the constraint database or not.",
"public": true
},
+ "WBQualityConstraintsSparqlEndpoint": {
+ "value":
"https://query.wikidata.org/bigdata/namespace/wdq/sparql",
+ "description": "The URL of the SPARQL endpoint. Should
accept the URL parameters 'query' and 'format'.",
+ "public": true
+ },
"WBQualityConstraintsInstanceOfId": {
"value": "P31",
"description": "The property ID of the 'instance of'
property, which specifies the class(es) of an item.",
diff --git a/i18n/en.json b/i18n/en.json
index fed8913..0b4ff22 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -58,6 +58,7 @@
"wbqc-violation-message-parameter-entity": "The value for the parameter
\"$1\" must be an entity, not \"$2\".",
"wbqc-violation-message-parameter-single": "The parameter \"$1\" must
only have a single value.",
"wbqc-violation-message-parameter-oneof": "The parameter \"$1\" must be
{{PLURAL:$2|1=$4.|2=either $4 or $5.|one of the following:$3}}",
+ "wbqc-violation-message-sparql-error": "The SPARQL query resulted in an
error.",
"wbqc-violation-message-commons-link-no-existent": "Commons link should
exist.",
"wbqc-violation-message-commons-link-not-well-formed": "Commons link
should be well-formed.",
@@ -86,5 +87,8 @@
"wbqc-violation-message-type-subclass": "Entities using the $1 property
should be subclasses of {{PLURAL:$3|1=$5|2=$5 or $6|one of the following
classes}} (or of {{PLURAL:$3|1=a subclass of it|2=a subclass of them|one of
their subclasses}}), but $2 currently {{PLURAL:$3|1=isn't.|2=isn't.|isn't:
$4}}",
"wbqc-violation-message-valueType-instance": "Values of $1 statements
should be instances of {{PLURAL:$3|1=$5|2=$5 or $6|one of the following
classes}} (or of {{PLURAL:$3|1=a subclass of it|2=a subclass of them|one of
their subclasses}}), but $2 currently {{PLURAL:$3|1=isn't.|2=isn't.|isn't:
$4}}",
"wbqc-violation-message-valueType-subclass": "Values of $1 statements
should be subclasses of {{PLURAL:$3|1=$5|2=$5 or $6|one of the following
classes}} (or of {{PLURAL:$3|1=a subclass of it|2=a subclass of them|one of
their subclasses}}), but $2 currently {{PLURAL:$3|1=isn't.|2=isn't.|isn't:
$4}}",
- "wbqc-violation-message-target-required-claim": "$1 should have
{{PLURAL:$3|0=a statement $2.|1=a statement $2 $5.|a statement for $2 with one
of the following values:$4}}"
+ "wbqc-violation-message-target-required-claim": "$1 should have
{{PLURAL:$3|0=a statement $2.|1=a statement $2 $5.|a statement for $2 with one
of the following values:$4}}",
+ "wbqc-violation-message-unique-value": "This property's value must not
be present on any other item.",
+ "wbqc-violation-message-sparql-type": "This property must only be used
on items that are in the relation to the item (or a subclass of the item)
defined in the parameters.",
+ "wbqc-violation-message-sparql-value-type": "This property's value
entity must be in the relation to the item (or a subclass of the item) defined
in the parameters."
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 84a07f8..2ed9efd 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -54,6 +54,7 @@
"wbqc-violation-message-parameter-entity": "Message for when the value
of a constraint parameter must be an entity, but is some other kind of data
value. $1 contains the parameter, $2 the data value type.",
"wbqc-violation-message-parameter-single": "Message for when a
constraint parameter has multiple values but only supports one. $1 contains the
parameter.",
"wbqc-violation-message-parameter-oneof": "Message for when a
constraint parameter must be one of several values, but is something different.
$1 contains the parameter, $2 the number of allowed values (possibly 1), $3 an
HTML list of all allowed values, and $4, $5 etc. are the individual allowed
values.{{Related|wbqc-violation-message-one-of}}",
+ "wbqc-violation-message-sparql-error": "Message for when a constraint
checker runs a SPARQL query but the SPARQL endpoint returns an error instead of
query results.",
"wbqc-violation-message-commons-link-no-existent": "Message for
violation of Commons link constraint. When linked commons page does not exist.",
"wbqc-violation-message-commons-link-not-well-formed": "Message for
violation of Commons link constraint. When link contains invalid characters.",
"wbqc-violation-message-commons-link-check-for-namespace-not-yet-implemented":
"Message for when the check for the Commons link constraint has not yet been
implemented for a specific namespace.",
@@ -81,5 +82,8 @@
"wbqc-violation-message-type-subclass": "Message for a violation of the
“Type” constraint, when the subject of a statement should have be a subclass of
a certain type but isn't. $1 is the property of the statement, $2 is the
subject of the statement, $3 is the number of classes, $4 is an HTML list of
all classes, and $5, $6 etc. are the individual
classes.{{Related|wbqc-violation-message-type-instance}}",
"wbqc-violation-message-valueType-instance": "Message for a violation
of the “Value type” constraint, when the value of a statement should have be an
instance of a certain type but isn't. $1 is the property of the statement, $2
is the value of the statement, $3 is the number of classes, $4 is an HTML list
of all classes, and $5, $6 etc. are the individual
classes.{{Related|wbqc-violation-message-valueType-subclass}}",
"wbqc-violation-message-valueType-subclass": "Message for a violation
of the “Value type” constraint, when the value of a statement should have be a
subclass of a certain type but isn't. $1 is the property of the statement, $2
is the value of the statement, $3 is the number of classes, $4 is an HTML list
of all classes, and $5, $6 etc. are the individual
classes.{{Related|wbqc-violation-message-valueType-instance}}",
- "wbqc-violation-message-target-required-claim": "Message for a
violation of the “Target required claim” constraint, when the target entity of
a statement is missing an expected statement. Parameters:\n* $1 is the subject
entity of the missing statement, i. e. the target entity of the statement that
has the constraint.\n* $2 is the property of the missing statement.\n* $3 is
the number of values permitted for the missing statement (or 0, in which case
the constraint only specifies that there should be a statement but not the
values it should have).\n* $4 is an HTML list of all values permitted for the
missing statement.\n* $5, $6 etc. are the individual values permitted for the
missing statement.\n{{Related|wbqc-violation-message-item}}"
+ "wbqc-violation-message-target-required-claim": "Message for a
violation of the “Target required claim” constraint, when the target entity of
a statement is missing an expected statement. Parameters:\n* $1 is the subject
entity of the missing statement, i. e. the target entity of the statement that
has the constraint.\n* $2 is the property of the missing statement.\n* $3 is
the number of values permitted for the missing statement (or 0, in which case
the constraint only specifies that there should be a statement but not the
values it should have).\n* $4 is an HTML list of all values permitted for the
missing statement.\n* $5, $6 etc. are the individual values permitted for the
missing statement.\n{{Related|wbqc-violation-message-item}}",
+ "wbqc-violation-message-unique-value": "Message for violation of the
Unique Value constraint, when other items are found.",
+ "wbqc-violation-message-sparql-type": "{{notranslate}}",
+ "wbqc-violation-message-sparql-value-type": "{{notranslate}}"
}
diff --git a/includes/ConstraintCheck/Checker/TypeSparqlChecker.php
b/includes/ConstraintCheck/Checker/TypeSparqlChecker.php
new file mode 100644
index 0000000..129d1ec
--- /dev/null
+++ b/includes/ConstraintCheck/Checker/TypeSparqlChecker.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
+
+use Wikibase\DataModel\Entity\EntityDocument;
+use Wikibase\DataModel\Services\Lookup\EntityLookup;
+use Wikibase\DataModel\Statement\StatementListProvider;
+use WikibaseQuality\ConstraintReport\Constraint;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
+use Wikibase\DataModel\Statement\Statement;
+
+/**
+ * @package WikibaseQuality\ConstraintReport\ConstraintCheck\Checker
+ * @author BP2014N1
+ * @license GNU GPL v2+
+ */
+class TypeSparqlChecker implements ConstraintChecker {
+
+ /**
+ * @var ConstraintParameterParser
+ */
+ private $helper;
+
+ /**
+ * @var EntityLookup
+ */
+ private $entityLookup;
+
+ /**
+ * @var SparqlHelper
+ */
+ private $sparqlHelper;
+
+ /**
+ * @param EntityLookup $lookup
+ * @param ConstraintParameterParser $helper
+ * @param SparqlHelper $sparqlHelper
+ */
+ public function __construct( EntityLookup $lookup,
ConstraintParameterParser $helper, SparqlHelper $sparqlHelper ) {
+ $this->entityLookup = $lookup;
+ $this->helper = $helper;
+ $this->sparqlHelper = $sparqlHelper;
+ }
+
+ /**
+ * Checks 'Type' constraint.
+ *
+ * @param Statement $statement
+ * @param Constraint $constraint
+ * @param EntityDocument|StatementListProvider $entity
+ *
+ * @return CheckResult
+ */
+ public function checkConstraint( Statement $statement, Constraint
$constraint, EntityDocument $entity = null ) {
+ $parameters = [];
+ $constraintParameters = $constraint->getConstraintParameters();
+
+ $classes = false;
+ if ( array_key_exists( 'class', $constraintParameters ) ) {
+ $classes = explode( ',', $constraintParameters['class']
);
+ $parameters['class'] =
$this->helper->parseParameterArray( $classes );
+ }
+
+ $relation = false;
+ if ( array_key_exists( 'relation', $constraintParameters ) ) {
+ $relation = $constraintParameters['relation'];
+ $parameters['relation'] =
$this->helper->parseSingleParameter( $relation, true );
+ }
+
+ /*
+ * error handling:
+ * parameter $constraintParameters['class'] must not be null
+ */
+ if ( !$classes ) {
+ $message = wfMessage(
"wbqc-violation-message-parameter-needed" )->params(
$constraint->getConstraintTypeName(), 'class' )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ /*
+ * error handling:
+ * parameter $constraintParameters['relation'] must be either
'instance' or 'subclass'
+ */
+ if ( $relation === 'instance' ) {
+ $withInstance = true;
+ } elseif ( $relation === 'subclass' ) {
+ $withInstance = false;
+ } else {
+ $message = wfMessage(
"wbqc-violation-message-type-relation-instance-or-subclass" )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ try {
+ if ( $this->sparqlHelper->hasType(
$entity->getId()->getSerialization(), $classes, $withInstance ) ) {
+ $message = '';
+ $status = CheckResult::STATUS_COMPLIANCE;
+ } else {
+ $message = wfMessage(
"wbqc-violation-message-sparql-type" )->escaped();
+ $status = CheckResult::STATUS_VIOLATION;
+ }
+ } catch ( SparqlHelperException $e ) {
+ $status = CheckResult::STATUS_VIOLATION;
+ $message = wfMessage(
'wbqc-violation-message-sparql-error' )->escaped();
+ }
+
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, $status, $message );
+ }
+
+}
diff --git a/includes/ConstraintCheck/Checker/UniqueValueChecker.php
b/includes/ConstraintCheck/Checker/UniqueValueChecker.php
index 907b68e..789f4fd 100644
--- a/includes/ConstraintCheck/Checker/UniqueValueChecker.php
+++ b/includes/ConstraintCheck/Checker/UniqueValueChecker.php
@@ -6,7 +6,8 @@
use Wikibase\DataModel\Statement\StatementListProvider;
use WikibaseQuality\ConstraintReport\Constraint;
use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
-use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ValueCountCheckerHelper;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException;
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
use Wikibase\DataModel\Statement\Statement;
@@ -18,18 +19,16 @@
class UniqueValueChecker implements ConstraintChecker {
/**
- * @var ValueCountCheckerHelper
+ * @var SparqlHelper
*/
- private $valueCountCheckerHelper;
+ private $sparqlHelper;
- public function __construct() {
- $this->valueCountCheckerHelper = new ValueCountCheckerHelper();
+ public function __construct( SparqlHelper $sparqlHelper ) {
+ $this->sparqlHelper = $sparqlHelper;
}
/**
* Checks 'Unique value' constraint.
- *
- * @todo Implement when index exists that makes it possible in
reasonable time.
*
* @param Statement $statement
* @param Constraint $constraint
@@ -40,8 +39,23 @@
public function checkConstraint( Statement $statement, Constraint
$constraint, EntityDocument $entity ) {
$parameters = [];
- $message = wfMessage(
"wbqc-violation-message-not-yet-implemented" )->params(
$constraint->getConstraintTypeName() )->escaped();
- return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_TODO, $message );
+ try {
+ $otherEntities =
$this->sparqlHelper->findEntitiesWithSameStatement( $statement );
+
+ if ( $otherEntities === [] ) {
+ $status = CheckResult::STATUS_COMPLIANCE;
+ $message = '';
+ } else {
+ $status = CheckResult::STATUS_VIOLATION;
+ // TODO include the other entities in the
message
+ $message = wfMessage(
'wbqc-violation-message-unique-value' )->escaped();
+ }
+ } catch ( SparqlHelperException $e ) {
+ $status = CheckResult::STATUS_VIOLATION;
+ $message = wfMessage(
'wbqc-violation-message-sparql-error' )->escaped();
+ }
+
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, $status, $message );
}
}
diff --git a/includes/ConstraintCheck/Checker/ValueTypeSparqlChecker.php
b/includes/ConstraintCheck/Checker/ValueTypeSparqlChecker.php
new file mode 100644
index 0000000..0a2c8b3
--- /dev/null
+++ b/includes/ConstraintCheck/Checker/ValueTypeSparqlChecker.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
+
+use Wikibase\DataModel\Entity\EntityDocument;
+use Wikibase\DataModel\Entity\EntityIdValue;
+use Wikibase\DataModel\Services\Lookup\EntityLookup;
+use Wikibase\DataModel\Snak\PropertyValueSnak;
+use Wikibase\DataModel\Statement\StatementListProvider;
+use WikibaseQuality\ConstraintReport\Constraint;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
+use Wikibase\DataModel\Statement\Statement;
+
+/**
+ * @package WikibaseQuality\ConstraintReport\ConstraintCheck\Checker
+ * @author BP2014N1
+ * @license GNU GPL v2+
+ */
+class ValueTypeSparqlChecker implements ConstraintChecker {
+
+ /**
+ * @var ConstraintParameterParser
+ */
+ private $helper;
+
+ /**
+ * @var EntityLookup
+ */
+ private $entityLookup;
+
+ /**
+ * @var SparqlHelper
+ */
+ private $sparqlHelper;
+
+ /**
+ * @param EntityLookup $lookup
+ * @param ConstraintParameterParser $helper
+ * @param SparqlHelper $sparqlHelper
+ */
+ public function __construct( EntityLookup $lookup,
ConstraintParameterParser $helper, SparqlHelper $sparqlHelper ) {
+ $this->entityLookup = $lookup;
+ $this->helper = $helper;
+ $this->sparqlHelper = $sparqlHelper;
+ }
+
+ /**
+ * Checks 'Value type' constraint.
+ *
+ * @param Statement $statement
+ * @param Constraint $constraint
+ * @param EntityDocument|StatementListProvider $entity
+ *
+ * @return CheckResult
+ */
+ public function checkConstraint( Statement $statement, Constraint
$constraint, EntityDocument $entity = null ) {
+ $parameters = [];
+ $constraintParameters = $constraint->getConstraintParameters();
+
+ $classes = false;
+ if ( array_key_exists( 'class', $constraintParameters ) ) {
+ $classes = explode( ',', $constraintParameters['class']
);
+ $parameters['class'] =
$this->helper->parseParameterArray( $classes );
+ }
+
+ $relation = false;
+ if ( array_key_exists( 'relation', $constraintParameters ) ) {
+ $relation = $constraintParameters['relation'];
+ $parameters['relation'] =
$this->helper->parseSingleParameter( $relation, true );
+ }
+
+ if ( array_key_exists( 'constraint_status',
$constraintParameters ) ) {
+ $parameters['constraint_status'] =
$this->helper->parseSingleParameter(
$constraintParameters['constraint_status'], true );
+ }
+
+ $mainSnak = $statement->getMainSnak();
+
+ /*
+ * error handling:
+ * $mainSnak must be PropertyValueSnak, neither
PropertySomeValueSnak nor PropertyNoValueSnak is allowed
+ */
+ if ( !$mainSnak instanceof PropertyValueSnak ) {
+ $message = wfMessage(
"wbqc-violation-message-value-needed" )->params(
$constraint->getConstraintTypeQid() )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ $dataValue = $mainSnak->getDataValue();
+
+ /*
+ * error handling:
+ * type of $dataValue for properties with 'Value type'
constraint has to be 'wikibase-entityid'
+ * parameter $constraintParameters['class'] must not be null
+ */
+ if ( $dataValue->getType() !== 'wikibase-entityid' ) {
+ $message = wfMessage(
"wbqc-violation-message-value-needed-of-type" )->params(
$constraint->getConstraintTypeQid(), 'wikibase-entityid' )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+ /** @var EntityIdValue $dataValue */
+
+ if ( !$classes ) {
+ $message = wfMessage(
"wbqc-violation-message-parameter-needed" )->params(
$constraint->getConstraintTypeQid(), 'class' )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ /*
+ * error handling:
+ * parameter $constraintParameters['relation'] must be either
'instance' or 'subclass'
+ */
+ if ( $relation === 'instance' ) {
+ $withInstance = true;
+ } elseif ( $relation === 'subclass' ) {
+ $withInstance = false;
+ } else {
+ $message = wfMessage(
"wbqc-violation-message-type-relation-instance-or-subclass" )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ $item = $this->entityLookup->getEntity(
$dataValue->getEntityId() );
+
+ if ( !( $item instanceof StatementListProvider ) ) {
+ $message = wfMessage(
"wbqc-violation-message-value-entity-must-exist" )->escaped();
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, CheckResult::STATUS_VIOLATION, $message );
+ }
+
+ try {
+ if ( $this->sparqlHelper->hasType(
$dataValue->getEntityId()->getSerialization(), $classes, $withInstance ) ) {
+ $message = '';
+ $status = CheckResult::STATUS_COMPLIANCE;
+ } else {
+ $message = wfMessage(
"wbqc-violation-message-sparql-value-type" )->escaped();
+ $status = CheckResult::STATUS_VIOLATION;
+ }
+ } catch ( SparqlHelperException $e ) {
+ $status = CheckResult::STATUS_VIOLATION;
+ $message = wfMessage(
'wbqc-violation-message-sparql-error' )->escaped();
+ }
+
+ return new CheckResult( $entity->getId(), $statement,
$constraint->getConstraintTypeQid(), $constraint->getConstraintId(),
$parameters, $status, $message );
+ }
+
+}
diff --git a/includes/ConstraintCheck/Helper/SparqlHelper.php
b/includes/ConstraintCheck/Helper/SparqlHelper.php
new file mode 100644
index 0000000..8060342
--- /dev/null
+++ b/includes/ConstraintCheck/Helper/SparqlHelper.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Helper;
+
+use Config;
+use Http;
+use MediaWiki\MediaWikiServices;
+use Wikibase\DataModel\Entity\EntityIdParser;
+use Wikibase\DataModel\Statement\Statement;
+use Wikibase\Rdf\RdfVocabulary;
+
+/**
+ * Class for running a SPARQL query on some endpoint and getting the results.
+ *
+ * @package WikibaseQuality\ConstraintReport\ConstraintCheck\Helper
+ * @author Lucas Werkmeister
+ * @license GNU GPL v2+
+ */
+class SparqlHelper {
+
+ const MAX_QUERY_SECONDS = 10;
+
+ /**
+ * @var Config
+ */
+ private $config;
+
+ /**
+ * @var string
+ */
+ private $entityPrefix;
+
+ /**
+ * @var string
+ */
+ private $prefixes;
+
+ /**
+ * @var EntityIdParser
+ */
+ private $entityIdParser;
+
+ public function __construct(
+ Config $config,
+ RdfVocabulary $rdfVocabulary,
+ EntityIdParser $entityIdParser
+ ) {
+ $this->config = $config;
+ $this->entityIdParser = $entityIdParser;
+
+ $this->entityPrefix = $rdfVocabulary->getNamespaceUri(
RdfVocabulary::NS_ENTITY );
+ $this->prefixes = <<<EOT
+PREFIX wd: <{$rdfVocabulary->getNamespaceUri( RdfVocabulary::NS_ENTITY )}>
+PREFIX wds: <{$rdfVocabulary->getNamespaceUri( RdfVocabulary::NS_STATEMENT )}>
+PREFIX wdt: <{$rdfVocabulary->getNamespaceUri( RdfVocabulary::NSP_DIRECT_CLAIM
)}>
+PREFIX p: <{$rdfVocabulary->getNamespaceUri( RdfVocabulary::NSP_CLAIM )}>
+PREFIX ps: <{$rdfVocabulary->getNamespaceUri(
RdfVocabulary::NSP_CLAIM_STATEMENT )}>
+EOT;
+ }
+
+ /**
+ * @param string $id entity ID serialization of the entity to check
+ * @param string[] $classes entity ID serializations of the expected
types
+ * @param boolean $withInstance true for “instance” relation, false for
“subclass” relation
+ * @return boolean
+ */
+ public function hasType( $id, array $classes, $withInstance ) {
+ $instanceOfId = $this->config->get(
'WBQualityConstraintsInstanceOfId' );
+ $subclassOfId = $this->config->get(
'WBQualityConstraintsSubclassOfId' );
+
+ $path = ( $withInstance ? "wdt:$instanceOfId/" : "" ) .
"wdt:$subclassOfId*";
+
+ foreach ( array_chunk( $classes, 20 ) as $classesChunk ) {
+ $classesValues = implode( ' ', array_map(
+ function( $class ) {
+ return 'wd:' . $class;
+ },
+ $classesChunk
+ ) );
+
+ $query = <<<EOF
+ASK {
+ BIND(wd:$id AS ?item)
+ VALUES ?class { $classesValues }
+ ?item $path ?class.
+}
+EOF;
+
+ $result = $this->runQuery( $query );
+ if ( $result['boolean'] ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Statement $statement
+ * @return EntityId?[]
+ */
+ public function findEntitiesWithSameStatement( Statement $statement ) {
+ $pid = $statement->getPropertyId()->serialize();
+ $guid = str_replace( '$', '-', $statement->getGuid() );
+
+ $query = <<<EOF
+SELECT ?otherEntity WHERE {
+ BIND(wds:$guid AS ?statement)
+ BIND(p:$pid AS ?p)
+ BIND(ps:$pid AS ?ps)
+ ?entity ?p ?statement.
+ ?statement ?ps ?value.
+ ?otherStatement ?ps ?value.
+ ?otherEntity ?p ?otherStatement.
+ FILTER(?otherEntity != ?entity)
+}
+LIMIT 10
+EOF;
+
+ $result = $this->runQuery( $query );
+
+ return array_map(
+ function( $resultBindings ) {
+ $entityIRI =
$resultBindings['otherEntity']['value'];
+ $entityPrefixLength = strlen(
$this->entityPrefix );
+ if ( substr( $entityIRI, 0, $entityPrefixLength
) === $this->entityPrefix ) {
+ try {
+ return
$this->entityIdParser->parse( substr( $entityIRI, $entityPrefixLength ) );
+ } catch ( EntityIdParsingException $e )
{
+ // fall through
+ }
+ }
+ return null;
+ },
+ $result['results']['bindings']
+ );
+ }
+
+ /**
+ * Runs a query against the configured endpoint and returns the results.
+ *
+ * @param string $query The query, unencoded (plain string).
+ *
+ * @return array The returned JSON data (you typically iterate over
["results"]["bindings"]).
+ */
+ public function runQuery( $query ) {
+
+ $endpoint = $this->config->get(
'WBQualityConstraintsSparqlEndpoint' );
+ $url = $endpoint . '?' . http_build_query(
+ [
+ 'query' => "#wbqc\n" . $this->prefixes . $query,
+ 'format' => 'json',
+ 'maxQueryTimeMillis' => self::MAX_QUERY_SECONDS
* 1000,
+ ],
+ null, ini_get( 'arg_separator.output' ),
+ // encode spaces with %20, not +
+ PHP_QUERY_RFC3986
+ );
+
+ $json = Http::get(
+ $url,
+ [
+ 'timeout' => self::MAX_QUERY_SECONDS + 1,
+ ]
+ );
+
+ if ( $json === false ) {
+ throw new SparqlHelperException();
+ }
+ $arr = json_decode( $json, true );
+ return $arr;
+ }
+
+}
diff --git a/includes/ConstraintCheck/Helper/SparqlHelperException.php
b/includes/ConstraintCheck/Helper/SparqlHelperException.php
new file mode 100644
index 0000000..fe36a7f
--- /dev/null
+++ b/includes/ConstraintCheck/Helper/SparqlHelperException.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Helper;
+
+use RuntimeException;
+
+/**
+ * @license GPL-2.0+
+ */
+class SparqlHelperException extends RuntimeException {
+
+ public function __construct() {
+ parent::__construct( 'The SPARQL query endpoint returned an
error.' );
+ }
+
+}
diff --git a/includes/ConstraintReportFactory.php
b/includes/ConstraintReportFactory.php
index 871c9ca..a1823c0 100644
--- a/includes/ConstraintReportFactory.php
+++ b/includes/ConstraintReportFactory.php
@@ -5,8 +5,10 @@
use Config;
use MediaWiki\MediaWikiServices;
use ValueFormatters\FormatterOptions;
+use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\Lib\SnakFormatter;
+use Wikibase\Rdf\RdfVocabulary;
use Wikibase\Repo\WikibaseRepo;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
@@ -28,10 +30,14 @@
use
WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\SingleValueChecker;
use WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\MultiValueChecker;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\UniqueValueChecker;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\TypeSparqlChecker;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\ValueTypeSparqlChecker;
use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConnectionCheckerHelper;
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\RangeCheckerHelper;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\TypeCheckerHelper;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\TypeCheckerSparqlHelper;
use Wikibase\DataModel\Services\Statement\StatementGuidParser;
class ConstraintReportFactory {
@@ -77,6 +83,16 @@
private $constraintParameterRenderer;
/**
+ * @var RdfVocabulary
+ */
+ private $rdfVocabulary;
+
+ /**
+ * @var EntityIdParser
+ */
+ private $entityIdParser;
+
+ /**
* Returns the default instance.
* IMPORTANT: Use only when it is not feasible to inject an instance
properly.
*
@@ -102,7 +118,9 @@
SnakFormatter::FORMAT_HTML,
new FormatterOptions()
)
- )
+ ),
+ $wikibaseRepo->getRdfVocabulary(),
+ $wikibaseRepo->getEntityIdParser()
);
}
@@ -113,12 +131,16 @@
EntityLookup $lookup,
StatementGuidParser $statementGuidParser,
Config $config,
- ConstraintParameterRenderer $constraintParameterRenderer
+ ConstraintParameterRenderer $constraintParameterRenderer,
+ RdfVocabulary $rdfVocabulary,
+ EntityIdParser $entityIdParser
) {
$this->lookup = $lookup;
$this->statementGuidParser = $statementGuidParser;
$this->config = $config;
$this->constraintParameterRenderer =
$constraintParameterRenderer;
+ $this->rdfVocabulary = $rdfVocabulary;
+ $this->entityIdParser = $entityIdParser;
}
/**
@@ -145,6 +167,11 @@
$connectionCheckerHelper = new
ConnectionCheckerHelper();
$rangeCheckerHelper = new RangeCheckerHelper();
$typeCheckerHelper = new TypeCheckerHelper(
$this->lookup, $this->config, $this->constraintParameterRenderer );
+ $sparqlHelper = new SparqlHelper(
+ $this->config,
+ $this->rdfVocabulary,
+ $this->entityIdParser
+ );
$this->constraintCheckerMap = [
'Conflicts with' => new ConflictsWithChecker(
$this->lookup, $constraintParameterParser, $connectionCheckerHelper,
$this->constraintParameterRenderer ),
@@ -161,10 +188,12 @@
'Value type' => new ValueTypeChecker(
$this->lookup, $constraintParameterParser, $typeCheckerHelper, $this->config ),
'Single value' => new SingleValueChecker(),
'Multi value' => new MultiValueChecker(),
- 'Unique value' => new UniqueValueChecker(),
+ 'Unique value' => new UniqueValueChecker(
$sparqlHelper ),
'Format' => new FormatChecker(
$constraintParameterParser ),
'Commons link' => new CommonsLinkChecker(
$constraintParameterParser ),
'One of' => new OneOfChecker(
$constraintParameterParser, $this->constraintParameterRenderer ),
+ 'Type (SPARQL)' => new TypeSparqlChecker(
$this->lookup, $constraintParameterParser, $sparqlHelper ),
+ 'Value type (SPARQL)' => new
ValueTypeSparqlChecker( $this->lookup, $constraintParameterParser,
$sparqlHelper ),
];
$this->constraintCheckerMap += [
$this->config->get(
'WBQualityConstraintsDistinctValuesConstraintId' ) =>
$this->constraintCheckerMap['Unique value'],
diff --git a/tests/phpunit/Checker/TypeChecker/Q42.json
b/tests/phpunit/Checker/TypeChecker/Q42.json
new file mode 100644
index 0000000..5dd13d5
--- /dev/null
+++ b/tests/phpunit/Checker/TypeChecker/Q42.json
@@ -0,0 +1,61 @@
+{
+ "pageid": 2076,
+ "ns": 0,
+ "title": "Q42",
+ "lastrevid": 11029,
+ "modified": "2015-04-01T13:51:03Z",
+ "id": "Q42",
+ "type": "item",
+ "labels": {
+ "en": {
+ "language": "en",
+ "value": "TestItemType"
+ }
+ },
+ "descriptions": {
+ "en": {
+ "language": "en",
+ "value": "Used for tests"
+ }
+ },
+ "claims": {
+ "P31": [
+ {
+ "id": "Q1$56e6a474-4431-fb24-cc15-1d580e467559",
+ "mainsnak": {
+ "snaktype": "value",
+ "property": "P31",
+ "datatype": "wikibase-item",
+ "datavalue": {
+ "value": {
+ "entity-type": "item",
+ "numeric-id": 100
+ },
+ "type": "wikibase-entityid"
+ }
+ },
+ "type": "statement",
+ "rank": "normal"
+ }
+ ],
+ "P279": [
+ {
+ "id": "Q1$e35707be-4a84-61fe-9b52-623784a316a7",
+ "mainsnak": {
+ "snaktype": "value",
+ "property": "P279",
+ "datatype": "wikibase-item",
+ "datavalue": {
+ "value": {
+ "entity-type": "item",
+ "numeric-id": 24
+ },
+ "type": "wikibase-entityid"
+ }
+ },
+ "type": "statement",
+ "rank": "normal"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/phpunit/Checker/TypeChecker/TypeSparqlCheckerTest.php
b/tests/phpunit/Checker/TypeChecker/TypeSparqlCheckerTest.php
new file mode 100644
index 0000000..7a72315
--- /dev/null
+++ b/tests/phpunit/Checker/TypeChecker/TypeSparqlCheckerTest.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\Test\TypeChecker;
+
+use PHPUnit_Framework_TestCase;
+use Wikibase\DataModel\Statement\Statement;
+use Wikibase\DataModel\Snak\PropertyValueSnak;
+use Wikibase\DataModel\Entity\EntityIdValue;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Entity\PropertyId;
+use WikibaseQuality\ConstraintReport\Constraint;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\TypeSparqlChecker;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use WikibaseQuality\ConstraintReport\Tests\DefaultConfig;
+use WikibaseQuality\ConstraintReport\Tests\ResultAssertions;
+use WikibaseQuality\ConstraintReport\Tests\SparqlHelperMock;
+use WikibaseQuality\Tests\Helper\JsonFileEntityLookup;
+
+/**
+ * @covers WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\TypeChecker
+ *
+ * @group WikibaseQualityConstraints
+ *
+ * @uses WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult
+ * @uses
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser
+ *
+ * @author Olga Bode
+ * @license GNU GPL v2+
+ */
+class TypeSparqlCheckerTest extends \PHPUnit_Framework_TestCase {
+
+ use DefaultConfig, ResultAssertions, SparqlHelperMock;
+
+ /**
+ * @var JsonFileEntityLookup
+ */
+ private $lookup;
+
+ /**
+ * @var TypeSparqlChecker
+ */
+ private $checker;
+
+ /**
+ * @var Statement
+ */
+ private $typeStatement;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->lookup = new JsonFileEntityLookup( __DIR__ );
+
+ $this->typeStatement = new Statement( new PropertyValueSnak(
new PropertyId( 'P1' ), new EntityIdValue( new ItemId( 'Q42' ) ) ) );
+ }
+
+ protected function tearDown() {
+ unset( $this->lookup );
+ unset( $this->typeStatement );
+ parent::tearDown();
+ }
+
+ // relation 'subclass'
+
+ public function testTypeConstraintSubclassValid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q1', [ 'Q100',
'Q101' ], false, true );
+
+ $this->checker = new TypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'class' => 'Q100,Q101',
+ 'relation' => 'subclass'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertCompliance( $checkResult );
+ }
+
+ // relation 'subclass', violations
+
+ public function testTypeConstraintSubclassIsInvalid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q1', [ 'Q200',
'Q201' ], false, false );
+
+ $this->checker = new TypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'class' => 'Q200,Q201',
+ 'relation' => 'subclass'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertViolation( $checkResult,
'wbqc-violation-message-sparql-type' );
+ }
+
+ /**
+ * @param string[] $parameters
+ *
+ * @return Constraint
+ */
+ private function getConstraintMock( array $parameters ) {
+ $mock = $this
+ ->getMockBuilder( Constraint::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintParameters' )
+ ->will( $this->returnValue( $parameters ) );
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintTypeQid' )
+ ->will( $this->returnValue( 'Type' ) );
+
+ return $mock;
+ }
+
+}
diff --git a/tests/phpunit/Checker/TypeChecker/ValueTypeSparqlCheckerTest.php
b/tests/phpunit/Checker/TypeChecker/ValueTypeSparqlCheckerTest.php
new file mode 100644
index 0000000..9db5b74
--- /dev/null
+++ b/tests/phpunit/Checker/TypeChecker/ValueTypeSparqlCheckerTest.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\Test\Checker;
+
+use PHPUnit_Framework_TestCase;
+use Wikibase\DataModel\Statement\Statement;
+use Wikibase\DataModel\Snak\PropertyValueSnak;
+use Wikibase\DataModel\Entity\EntityIdValue;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Entity\PropertyId;
+use WikibaseQuality\ConstraintReport\Constraint;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\ValueTypeSparqlChecker;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use WikibaseQuality\ConstraintReport\Tests\DefaultConfig;
+use WikibaseQuality\ConstraintReport\Tests\ResultAssertions;
+use WikibaseQuality\ConstraintReport\Tests\SparqlHelperMock;
+use WikibaseQuality\Tests\Helper\JsonFileEntityLookup;
+
+/**
+ * @covers WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\TypeChecker
+ *
+ * @group WikibaseQualityConstraints
+ *
+ * @uses WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult
+ * @uses
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser
+ *
+ * @author Olga Bode
+ * @license GNU GPL v2+
+ */
+class ValueTypeCheckerSparqlTest extends \PHPUnit_Framework_TestCase {
+
+ use DefaultConfig, ResultAssertions, SparqlHelperMock;
+
+ /**
+ * @var JsonFileEntityLookup
+ */
+ private $lookup;
+
+ /**
+ * @var ValueTypeSparqlChecker
+ */
+ private $checker;
+
+ /**
+ * @var PropertyId
+ */
+ private $valueTypePropertyId;
+
+ /**
+ * @var Statement
+ */
+ private $typeStatement;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->lookup = new JsonFileEntityLookup( __DIR__ );
+ $this->valueTypePropertyId = new PropertyId( 'P1234' );
+ $this->typeStatement = new Statement( new PropertyValueSnak(
new PropertyId( 'P1' ), new EntityIdValue( new ItemId( 'Q42' ) ) ) );
+ }
+
+ protected function tearDown() {
+ unset( $this->lookup );
+ unset( $this->typeStatement );
+ unset( $this->valueTypePropertyId );
+ parent::tearDown();
+ }
+
+ // relation 'subclass'
+
+ public function testValueTypeConstraintSubclassValid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q42', [ 'Q100',
'Q101' ], false, true );
+
+ $this->checker = new ValueTypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'class' => 'Q100,Q101',
+ 'relation' => 'subclass'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertCompliance( $checkResult );
+ }
+
+ // relation 'subclass', violations
+
+ public function testValueTypeConstraintSubclassInvalid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q42', [ 'Q200',
'Q201' ], false, false );
+
+ $this->checker = new ValueTypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'class' => 'Q200,Q201',
+ 'relation' => 'subclass'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertViolation( $checkResult,
'wbqc-violation-message-sparql-value-type' );
+ }
+
+ // relation 'instance'
+
+ public function testValueTypeConstraintInstanceValid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q42', [ 'Q100',
'Q101' ], true, true );
+
+ $this->checker = new ValueTypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'relation' => 'instance',
+ 'class' => 'Q100,Q101'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertCompliance( $checkResult );
+ }
+
+ // relation 'instance', violations
+
+ public function testValueTypeConstraintInstanceInvalid() {
+ $mock = $this->getSparqlHelperMockHasType( 'Q42', [ 'Q200',
'Q201' ], true, false );
+
+ $this->checker = new ValueTypeSparqlChecker(
+ $this->lookup,
+ new ConstraintParameterParser(),
+ $mock
+ );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $constraintParameters = [
+ 'relation' => 'instance',
+ 'class' => 'Q200,Q201'
+ ];
+
+ $checkResult = $this->checker->checkConstraint(
$this->typeStatement, $this->getConstraintMock( $constraintParameters ),
$entity );
+ $this->assertViolation( $checkResult,
'wbqc-violation-message-sparql-value-type' );
+ }
+
+ /**
+ * @param string[] $parameters
+ *
+ * @return Constraint
+ */
+ private function getConstraintMock( array $parameters ) {
+ $mock = $this
+ ->getMockBuilder( Constraint::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintParameters' )
+ ->will( $this->returnValue( $parameters ) );
+ $mock->expects( $this->any() )
+ ->method( 'getConstraintTypeQid' )
+ ->will( $this->returnValue( 'Type' ) );
+
+ return $mock;
+ }
+
+}
diff --git a/tests/phpunit/Checker/ValueCountChecker/UniqueValueCheckerTest.php
b/tests/phpunit/Checker/ValueCountChecker/UniqueValueCheckerTest.php
index c7f98e7..50b085d 100644
--- a/tests/phpunit/Checker/ValueCountChecker/UniqueValueCheckerTest.php
+++ b/tests/phpunit/Checker/ValueCountChecker/UniqueValueCheckerTest.php
@@ -7,10 +7,15 @@
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\PropertyId;
+use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Entity\EntityIdValue;
use WikibaseQuality\ConstraintReport\Constraint;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\UniqueValueChecker;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use
WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
use WikibaseQuality\ConstraintReport\Tests\ResultAssertions;
+use WikibaseQuality\ConstraintReport\Tests\SparqlHelperMock;
+use WikibaseQuality\Tests\Helper\JsonFileEntityLookup;
/**
* @covers
\WikibaseQuality\ConstraintReport\ConstraintCheck\Checker\UniqueValueChecker
@@ -19,10 +24,17 @@
*
* @uses \WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult
*
- * @author BP2014N1
+ * @author Olga Bode
* @license GNU GPL v2+
*/
-class UniqueValueCheckerTest extends \MediaWikiTestCase {
+class UniqueValueCheckerTest extends \PHPUnit_Framework_TestCase {
+
+ use SparqlHelperMock;
+
+ /**
+ * @var JsonFileEntityLookup
+ */
+ private $lookup;
use ResultAssertions;
@@ -38,23 +50,43 @@
protected function setUp() {
parent::setUp();
+ $this->lookup = new JsonFileEntityLookup( __DIR__ );
+ $this->uniquePropertyId = new PropertyId( 'P31' );
- $this->uniquePropertyId = new PropertyId( 'P227' );
- $this->checker = new UniqueValueChecker();
}
protected function tearDown() {
unset( $this->uniquePropertyId );
+ unset( $this->lookup );
parent::tearDown();
}
- public function testCheckUniqueValueConstraint() {
- $itemId = new ItemId( 'Q404' );
- $entity = new Item( $itemId );
- $statement = new Statement( new PropertyValueSnak(
$this->uniquePropertyId, new EntityIdValue( $itemId ) ) );
- $checkResult = $this->checker->checkConstraint( $statement,
$this->getConstraintMock( [] ), $entity );
+ public function testCheckUniqueValueConstraintInvalid() {
+ $statement = new Statement( new PropertyValueSnak(
$this->uniquePropertyId, new EntityIdValue( new ItemId( 'Q6' ) ) ) );
+ $statement->setGuid( 'Q6$e35707be-4a84-61fe-9b52-623784a316a7'
);
- $this->assertTodo( $checkResult );
+ $mock = $this->getSparqlHelperMockFindEntities( $statement, [
new ItemId( 'Q42' ) ] );
+
+ $this->checker = new UniqueValueChecker( $mock );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q6' ) );
+
+ $checkResult = $this->checker->checkConstraint( $statement,
$this->getConstraintMock( [] ), $entity );
+ $this->assertViolation( $checkResult,
'wbqc-violation-message-unique-value' );
+ }
+
+ public function testCheckUniqueValueConstraintValid() {
+ $statement = new Statement( new PropertyValueSnak(
$this->uniquePropertyId, new EntityIdValue( new ItemId( 'Q1' ) ) ) );
+ $statement->setGuid( "Q1$56e6a474-4431-fb24-cc15-1d580e467559"
);
+
+ $mock = $this->getSparqlHelperMockFindEntities( $statement, []
);
+
+ $this->checker = new UniqueValueChecker( $mock );
+
+ $entity = $this->lookup->getEntity( new ItemId( 'Q1' ) );
+
+ $checkResult = $this->checker->checkConstraint( $statement,
$this->getConstraintMock( [] ), $entity );
+ $this->assertCompliance( $checkResult );
}
/**
diff --git a/tests/phpunit/DefaultConfig.php b/tests/phpunit/DefaultConfig.php
index cc49b92..b3812ad 100644
--- a/tests/phpunit/DefaultConfig.php
+++ b/tests/phpunit/DefaultConfig.php
@@ -26,6 +26,8 @@
}
// reduce some limits to make tests run faster
$this->defaultConfig->set(
'WBQualityConstraintsTypeCheckMaxEntities', 10 );
+ // never query remote servers
+ $this->defaultConfig->set(
'WBQualityConstraintsSparqlEndpoint', 'http://localhost:65536/' );
}
return $this->defaultConfig;
diff --git a/tests/phpunit/DelegatingConstraintCheckerTest.php
b/tests/phpunit/DelegatingConstraintCheckerTest.php
index ee1e980..dc373de 100644
--- a/tests/phpunit/DelegatingConstraintCheckerTest.php
+++ b/tests/phpunit/DelegatingConstraintCheckerTest.php
@@ -5,6 +5,7 @@
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\ItemIdParser;
use Wikibase\DataModel\Services\Statement\StatementGuidParser;
+use Wikibase\Rdf\RdfVocabulary;
use
WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
use WikibaseQuality\ConstraintReport\ConstraintReportFactory;
use WikibaseQuality\ConstraintReport\Tests\ConstraintParameters;
@@ -63,13 +64,20 @@
protected function setUp() {
parent::setUp();
$this->lookup = $this->createEntityLookup();
- $this->statementGuidParser = new StatementGuidParser( new
ItemIdParser() );
+ $itemIdParser = new ItemIdParser();
+ $this->statementGuidParser = new StatementGuidParser(
$itemIdParser );
$config = $this->getDefaultConfig();
+ $rdfVocabulary = new RdfVocabulary(
+ 'http://www.wikidata.org/entity/',
+ 'http://www.wikidata.org/wiki/Special:EntityData/'
+ );
$factory = new ConstraintReportFactory(
$this->lookup,
$this->statementGuidParser,
$config,
- $this->getConstraintParameterRenderer()
+ $this->getConstraintParameterRenderer(),
+ $rdfVocabulary,
+ $itemIdParser
);
$this->constraintChecker = $factory->getConstraintChecker();
@@ -80,6 +88,7 @@
protected function tearDown() {
unset( $this->lookup );
unset( $this->constraintChecker );
+ unset( $this->statementGuidParser );
parent::tearDown();
}
diff --git a/tests/phpunit/Helper/SparqlHelperTest.php
b/tests/phpunit/Helper/SparqlHelperTest.php
new file mode 100644
index 0000000..e52e7f2
--- /dev/null
+++ b/tests/phpunit/Helper/SparqlHelperTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\Test\Helper;
+
+use Wikibase\DataModel\Entity\EntityIdValue;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Entity\ItemIdParser;
+use Wikibase\DataModel\Entity\PropertyId;
+use Wikibase\DataModel\Snak\PropertyValueSnak;
+use Wikibase\DataModel\Statement\Statement;
+use Wikibase\Rdf\RdfVocabulary;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+use WikibaseQuality\ConstraintReport\Tests\DefaultConfig;
+
+/**
+ * @covers
\WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper
+ *
+ * @group WikibaseQualityConstraints
+ *
+ * @author Lucas Werkmeister
+ * @license GNU GPL v2+
+ */
+class SparqlHelperTest extends \PHPUnit_Framework_TestCase {
+
+ use DefaultConfig;
+
+ public function testHasType() {
+ $sparqlHelper = $this->getMockBuilder( SparqlHelper::class )
+ ->setConstructorArgs( [
+ $this->getDefaultConfig(),
+ new RdfVocabulary(
+
'http://www.wikidata.org/entity/',
+
'http://www.wikidata.org/wiki/Special:EntityData/'
+ ),
+ new ItemIdParser()
+ ] )
+ ->setMethods( [ 'runQuery' ] )
+ ->getMock();
+
+ $query = <<<EOF
+ASK {
+ BIND(wd:Q1 AS ?item)
+ VALUES ?class { wd:Q100 wd:Q101 }
+ ?item wdt:P31/wdt:P279* ?class.
+}
+EOF;
+
+ $sparqlHelper->expects( $this->exactly( 1 ) )
+ ->method( 'runQuery' )
+ ->willReturn( [ 'boolean' => true ] )
+ ->withConsecutive( [ $this->equalTo( $query ) ] );
+
+ $this->assertTrue( $sparqlHelper->hasType( 'Q1', [ 'Q100',
'Q101' ], true ) );
+ }
+
+ public function testFindEntitiesWithSameStatement() {
+ $guid = 'Q1$8542690f-dfab-4846-944f-8382df730d2c';
+ $statement = new Statement(
+ new PropertyValueSnak( new PropertyId( 'P1' ), new
EntityIdValue( new ItemId( 'Q1' ) ) ),
+ null,
+ null,
+ $guid
+ );
+
+ $sparqlHelper = $this->getMockBuilder( SparqlHelper::class )
+ ->setConstructorArgs( [
+ $this->getDefaultConfig(),
+ new RdfVocabulary(
+
'http://www.wikidata.org/entity/',
+
'http://www.wikidata.org/wiki/Special:EntityData/'
+ ),
+ new ItemIdParser()
+ ] )
+ ->setMethods( [ 'runQuery' ] )
+ ->getMock();
+
+ $query = <<<EOF
+SELECT ?otherEntity WHERE {
+ BIND(wds:Q1-8542690f-dfab-4846-944f-8382df730d2c AS ?statement)
+ BIND(p:P1 AS ?p)
+ BIND(ps:P1 AS ?ps)
+ ?entity ?p ?statement.
+ ?statement ?ps ?value.
+ ?otherStatement ?ps ?value.
+ ?otherEntity ?p ?otherStatement.
+ FILTER(?otherEntity != ?entity)
+}
+LIMIT 10
+EOF;
+
+ $sparqlHelper->expects( $this->exactly( 1 ) )
+ ->method( 'runQuery' )
+ ->willReturn( [ 'head' => [ 'vars' => [ 'otherEntity' ]
], 'results' => [ 'bindings' => [
+ [ 'otherEntity' => [ 'type' => 'uri', 'value'
=> 'http://www.wikidata.org/entity/Q100' ] ],
+ [ 'otherEntity' => [ 'type' => 'uri', 'value'
=> 'http://www.wikidata.org/entity/Q101' ] ],
+ ] ] ] )
+ ->withConsecutive( [ $this->equalTo( $query ) ] );
+
+ $this->assertEquals(
+ $sparqlHelper->findEntitiesWithSameStatement(
$statement ),
+ [ new ItemId( 'Q100' ), new ItemId( 'Q101' ) ]
+ );
+ }
+
+}
diff --git a/tests/phpunit/SparqlHelperMock.php
b/tests/phpunit/SparqlHelperMock.php
new file mode 100644
index 0000000..51dc540
--- /dev/null
+++ b/tests/phpunit/SparqlHelperMock.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace WikibaseQuality\ConstraintReport\Tests;
+
+use HashConfig;
+use Wikibase\DataModel\Statement\Statement;
+use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
+
+/**
+ * @author Lucas Werkmeister
+ * @license GNU GPL v2+
+ */
+trait SparqlHelperMock {
+
+ /**
+ * @param string $expectedId
+ * @param string[] $expectedClasses
+ * @param boolean $expectedWithInstance
+ * @param boolean $result
+ * @return SparqlHelper
+ */
+ private function getSparqlHelperMockHasType(
+ $expectedId, array $expectedClasses, $expectedWithInstance,
+ $result ) {
+
+ $mock = $this->getMockBuilder( SparqlHelper::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock->expects( $this->exactly( 1 ) )
+ ->method( 'hasType' )
+ ->willReturn( $result )
+ ->withConsecutive( [
+ $this->equalTo( $expectedId ),
+ $this->equalTo( $expectedClasses ),
+ $this->equalTo( $expectedWithInstance )
+ ] );
+
+ return $mock;
+ }
+
+ /**
+ * @param Statement $expectedStatement
+ * @param EntityId?[] $result
+ * @return SparqlHelper
+ */
+ private function getSparqlHelperMockFindEntities(
+ Statement $expectedStatement,
+ $result ) {
+
+ $mock = $this->getMockBuilder( SparqlHelper::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock->expects( $this->exactly( 1 ) )
+ ->method( 'findEntitiesWithSameStatement' )
+ ->willReturn( $result )
+ ->withConsecutive( [ $this->equalTo( $expectedStatement
) ] );
+
+ return $mock;
+ }
+
+}
--
To view, visit https://gerrit.wikimedia.org/r/345302
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8af2e92215c026805df64babc5baf2a4ac8bc25e
Gerrit-PatchSet: 33
Gerrit-Project: mediawiki/extensions/WikibaseQualityConstraints
Gerrit-Branch: master
Gerrit-Owner: Olga Bode <[email protected]>
Gerrit-Reviewer: Jonas Kress (WMDE) <[email protected]>
Gerrit-Reviewer: Lucas Werkmeister (WMDE) <[email protected]>
Gerrit-Reviewer: Olga Bode <[email protected]>
Gerrit-Reviewer: Smalyshev <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits