Unverified Commit e4213652 authored by Andreas Braun's avatar Andreas Braun

Merge pull request #661

parents 7880bcd1 016eb3e4
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6.4 || ^8.3", "phpunit/phpunit": "^5.7.27 || ^6.4 || ^8.3",
"sebastian/comparator": "^1.0 || ^2.0 || ^3.0",
"symfony/phpunit-bridge": "^4.4@dev" "symfony/phpunit-bridge": "^4.4@dev"
}, },
"autoload": { "autoload": {
......
...@@ -8,6 +8,8 @@ use PHPUnit\Framework\Constraint\Constraint; ...@@ -8,6 +8,8 @@ use PHPUnit\Framework\Constraint\Constraint;
use ArrayObject; use ArrayObject;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Comparator\Factory;
use stdClass; use stdClass;
use Symfony\Bridge\PhpUnit\ConstraintTrait; use Symfony\Bridge\PhpUnit\ConstraintTrait;
...@@ -31,6 +33,12 @@ class DocumentsMatchConstraint extends Constraint ...@@ -31,6 +33,12 @@ class DocumentsMatchConstraint extends Constraint
private $sortKeys = false; private $sortKeys = false;
private $value; private $value;
/** @var ComparisonFailure|null */
private $lastFailure;
/** @var Factory */
private $comparatorFactory;
/** /**
* Creates a new constraint. * Creates a new constraint.
* *
...@@ -45,6 +53,44 @@ class DocumentsMatchConstraint extends Constraint ...@@ -45,6 +53,44 @@ class DocumentsMatchConstraint extends Constraint
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot; $this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded; $this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
$this->placeholders = $placeholders; $this->placeholders = $placeholders;
$this->comparatorFactory = Factory::getInstance();
}
public function evaluate($other, $description = '', $returnResult = false)
{
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
* able to skip preparation, convert both documents to extended JSON,
* and compare strings.
*
* If ignoreExtraKeys is false and sortKeys is true, we still be able to
* compare JSON strings but will still require preparation to sort keys
* in all documents and sub-documents. */
$other = $this->prepareBSON($other, true, $this->sortKeys);
$success = false;
$this->lastFailure = null;
try {
$this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
$success = true;
} catch (RuntimeException $e) {
$this->lastFailure = new ComparisonFailure(
$this->value,
$other,
$this->exporter()->export($this->value),
$this->exporter()->export($other),
false,
$e->getMessage()
);
}
if ($returnResult) {
return $success;
}
if (!$success) {
$this->fail($other, $description, $this->lastFailure);
}
} }
/** /**
...@@ -53,19 +99,24 @@ class DocumentsMatchConstraint extends Constraint ...@@ -53,19 +99,24 @@ class DocumentsMatchConstraint extends Constraint
* @param ArrayObject $expected * @param ArrayObject $expected
* @param ArrayObject $actual * @param ArrayObject $actual
* @param boolean $ignoreExtraKeys * @param boolean $ignoreExtraKeys
* @param string $keyPrefix
* @throws RuntimeException if the documents do not match * @throws RuntimeException if the documents do not match
*/ */
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys) private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys, $keyPrefix = '')
{ {
if (get_class($expected) !== get_class($actual)) { if (get_class($expected) !== get_class($actual)) {
throw new RuntimeException(sprintf('$expected is %s but $actual is %s', get_class($expected), get_class($actual))); throw new RuntimeException(sprintf(
'%s is not instance of expected class "%s"',
$this->exporter()->shortenedExport($actual),
get_class($expected)
));
} }
foreach ($expected as $key => $expectedValue) { foreach ($expected as $key => $expectedValue) {
$actualHasKey = $actual->offsetExists($key); $actualHasKey = $actual->offsetExists($key);
if (!$actualHasKey) { if (!$actualHasKey) {
throw new RuntimeException('$actual is missing key: ' . $key); throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
} }
if (in_array($expectedValue, $this->placeholders, true)) { if (in_array($expectedValue, $this->placeholders, true)) {
...@@ -76,12 +127,53 @@ class DocumentsMatchConstraint extends Constraint ...@@ -76,12 +127,53 @@ class DocumentsMatchConstraint extends Constraint
if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) || if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) ||
($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) { ($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded); $this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded, $keyPrefix . $key . '.');
continue; continue;
} }
if (gettype($expectedValue) != gettype($actualValue) || $expectedValue != $actualValue) { if (is_scalar($expectedValue) && is_scalar($actualValue)) {
throw new RuntimeException('$expectedValue != $actualValue for key: ' . $key); if ($expectedValue !== $actualValue) {
throw new ComparisonFailure(
$expectedValue,
$actualValue,
'',
'',
false,
sprintf('Field path "%s": %s', $keyPrefix . $key, 'Failed asserting that two values are equal.')
);
}
continue;
}
// Workaround for ObjectComparator printing the whole actual object
if (get_class($expectedValue) !== get_class($actualValue)) {
throw new ComparisonFailure(
$expectedValue,
$actualValue,
'',
'',
false,
\sprintf(
'Field path "%s": %s is not instance of expected class "%s".',
$keyPrefix . $key,
$this->exporter()->shortenedExport($actualValue),
get_class($expectedValue)
)
);
}
try {
$this->comparatorFactory->getComparatorFor($expectedValue, $actualValue)->assertEquals($expectedValue, $actualValue);
} catch (ComparisonFailure $failure) {
throw new ComparisonFailure(
$expectedValue,
$actualValue,
'',
'',
false,
sprintf('Field path "%s": %s', $keyPrefix . $key, $failure->getMessage())
);
} }
} }
...@@ -91,11 +183,25 @@ class DocumentsMatchConstraint extends Constraint ...@@ -91,11 +183,25 @@ class DocumentsMatchConstraint extends Constraint
foreach ($actual as $key => $value) { foreach ($actual as $key => $value) {
if (!$expected->offsetExists($key)) { if (!$expected->offsetExists($key)) {
throw new RuntimeException('$actual has extra key: ' . $key); throw new RuntimeException(sprintf('$actual has extra key: "%s"', $keyPrefix . $key));
} }
} }
} }
private function doAdditionalFailureDescription($other)
{
if ($this->lastFailure === null) {
return '';
}
return $this->lastFailure->getMessage();
}
private function doFailureDescription($other)
{
return 'two BSON objects are equal';
}
private function doMatches($other) private function doMatches($other)
{ {
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be /* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
...@@ -118,7 +224,7 @@ class DocumentsMatchConstraint extends Constraint ...@@ -118,7 +224,7 @@ class DocumentsMatchConstraint extends Constraint
private function doToString() private function doToString()
{ {
return 'matches ' . json_encode($this->value); return 'matches ' . $this->exporter()->export($this->value);
} }
/** /**
......
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
namespace MongoDB\Tests\SpecTests; namespace MongoDB\Tests\SpecTests;
use PHPUnit\Framework\TestCase; use ArrayObject;
use MongoDB\Model\BSONArray;
use MongoDB\Tests\TestCase;
use PHPUnit\Framework\ExpectationFailedException;
class DocumentsMatchConstraintTest extends TestCase class DocumentsMatchConstraintTest extends TestCase
{ {
...@@ -16,7 +19,7 @@ class DocumentsMatchConstraintTest extends TestCase ...@@ -16,7 +19,7 @@ class DocumentsMatchConstraintTest extends TestCase
$this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are not permitted'); $this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are not permitted');
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded key order is not significant'); $this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded key order is not significant');
// Arrays are always intepretted as root documents // Arrays are always interpreted as root documents
$c = new DocumentsMatchConstraint([1, ['a' => 1]], true, false); $c = new DocumentsMatchConstraint([1, ['a' => 1]], true, false);
$this->assertResult(false, $c, [1, 2], 'Incorrect value'); $this->assertResult(false, $c, [1, 2], 'Incorrect value');
...@@ -36,7 +39,7 @@ class DocumentsMatchConstraintTest extends TestCase ...@@ -36,7 +39,7 @@ class DocumentsMatchConstraintTest extends TestCase
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are permitted'); $this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are permitted');
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded Key order is not significant'); $this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded Key order is not significant');
// Arrays are always intepretted as root documents // Arrays are always interpreted as root documents
$c = new DocumentsMatchConstraint([1, ['a' => 1]], false, true); $c = new DocumentsMatchConstraint([1, ['a' => 1]], false, true);
$this->assertResult(false, $c, [1, 2], 'Incorrect value'); $this->assertResult(false, $c, [1, 2], 'Incorrect value');
...@@ -55,6 +58,56 @@ class DocumentsMatchConstraintTest extends TestCase ...@@ -55,6 +58,56 @@ class DocumentsMatchConstraintTest extends TestCase
$this->assertResult(true, $c, ['x' => '42', 'y' => 42, 'z' => ['a' => 24]], 'Exact match'); $this->assertResult(true, $c, ['x' => '42', 'y' => 42, 'z' => ['a' => 24]], 'Exact match');
} }
/**
* @dataProvider errorMessageProvider
*/
public function testErrorMessages($expectedMessagePart, DocumentsMatchConstraint $constraint, $actualValue)
{
try {
$constraint->evaluate($actualValue);
$this->fail('Expected a comparison failure');
} catch (ExpectationFailedException $failure) {
$this->assertStringContainsString('Failed asserting that two BSON objects are equal.', $failure->getMessage());
$this->assertStringContainsString($expectedMessagePart, $failure->getMessage());
}
}
public function errorMessageProvider()
{
return [
'Root type mismatch' => [
'MongoDB\Model\BSONArray Object (...) is not instance of expected class "MongoDB\Model\BSONDocument"',
new DocumentsMatchConstraint(['foo' => 'bar']),
new BSONArray(['foo' => 'bar']),
],
'Missing key' => [
'$actual is missing key: "foo.bar"',
new DocumentsMatchConstraint(['foo' => ['bar' => 'baz']]),
['foo' => ['foo' => 'bar']],
],
'Extra key' => [
'$actual has extra key: "foo.foo"',
new DocumentsMatchConstraint(['foo' => ['bar' => 'baz']]),
['foo' => ['foo' => 'bar', 'bar' => 'baz']],
],
'Scalar value not equal' => [
'Field path "foo": Failed asserting that two values are equal.',
new DocumentsMatchConstraint(['foo' => 'bar']),
['foo' => 'baz'],
],
'Scalar type mismatch' => [
'Field path "foo": Failed asserting that two values are equal.',
new DocumentsMatchConstraint(['foo' => 42]),
['foo' => '42'],
],
'Type mismatch' => [
'Field path "foo": MongoDB\Model\BSONDocument Object (...) is not instance of expected class "MongoDB\Model\BSONArray".',
new DocumentsMatchConstraint(['foo' => ['bar']]),
['foo' => (object) ['bar']],
],
];
}
private function assertResult($expectedResult, DocumentsMatchConstraint $constraint, $value, $message) private function assertResult($expectedResult, DocumentsMatchConstraint $constraint, $value, $message)
{ {
$this->assertSame($expectedResult, $constraint->evaluate($value, '', true), $message); $this->assertSame($expectedResult, $constraint->evaluate($value, '', true), $message);
......
...@@ -17,3 +17,7 @@ if ( ! class_exists(PHPUnit\Framework\Error\Warning::class)) { ...@@ -17,3 +17,7 @@ if ( ! class_exists(PHPUnit\Framework\Error\Warning::class)) {
if ( ! class_exists(PHPUnit\Framework\Constraint\Constraint::class)) { if ( ! class_exists(PHPUnit\Framework\Constraint\Constraint::class)) {
class_alias(PHPUnit_Framework_Constraint::class, PHPUnit\Framework\Constraint\Constraint::class); class_alias(PHPUnit_Framework_Constraint::class, PHPUnit\Framework\Constraint\Constraint::class);
} }
if ( ! class_exists(PHPUnit\Framework\ExpectationFailedException::class)) {
class_alias(PHPUnit_Framework_ExpectationFailedException::class, PHPUnit\Framework\ExpectationFailedException::class);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment