Skip to content

Commit ece69fb

Browse files
authored
PHPLIB-715: Use flexible numeric comparisons in DocumentMatchConstraint (#958)
* PHPLIB-715: Use flexible numeric comparisons in DocumentMatchConstraint * Fix phpcs
1 parent 0c3bc18 commit ece69fb

File tree

5 files changed

+38
-43
lines changed

5 files changed

+38
-43
lines changed

tests/SpecTests/DocumentsMatchConstraint.php

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
use function array_values;
3636
use function get_class;
3737
use function get_debug_type;
38-
use function in_array;
3938
use function is_array;
39+
use function is_float;
40+
use function is_int;
4041
use function is_object;
41-
use function is_scalar;
4242
use function method_exists;
4343
use function sprintf;
4444

@@ -59,9 +59,6 @@ class DocumentsMatchConstraint extends Constraint
5959
/** @var boolean */
6060
private $ignoreExtraKeysInEmbedded = false;
6161

62-
/** @var array */
63-
private $placeholders = [];
64-
6562
/**
6663
* TODO: This is not currently used, but was preserved from the design of
6764
* TestCase::assertMatchesDocument(), which would sort keys and then compare
@@ -88,14 +85,12 @@ class DocumentsMatchConstraint extends Constraint
8885
* @param array|object $value
8986
* @param boolean $ignoreExtraKeysInRoot If true, ignore extra keys within the root document
9087
* @param boolean $ignoreExtraKeysInEmbedded If true, ignore extra keys within embedded documents
91-
* @param array $placeholders Placeholders for any value
9288
*/
93-
public function __construct($value, bool $ignoreExtraKeysInRoot = false, bool $ignoreExtraKeysInEmbedded = false, array $placeholders = [])
89+
public function __construct($value, bool $ignoreExtraKeysInRoot = false, bool $ignoreExtraKeysInEmbedded = false)
9490
{
9591
$this->value = $this->prepareBSON($value, true, $this->sortKeys);
9692
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
9793
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
98-
$this->placeholders = $placeholders;
9994
$this->comparatorFactory = Factory::getInstance();
10095
}
10196

@@ -303,10 +298,6 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, bool $
303298
throw new RuntimeException(sprintf('$actual is missing key: "%s"', $keyPrefix . $key));
304299
}
305300

306-
if (in_array($expectedValue, $this->placeholders, true)) {
307-
continue;
308-
}
309-
310301
$actualValue = $actual[$key];
311302

312303
if ($expectedValue instanceof BSONDocument && isset($expectedValue['$$type'])) {
@@ -322,26 +313,13 @@ private function assertEquals(ArrayObject $expected, ArrayObject $actual, bool $
322313
continue;
323314
}
324315

325-
if (is_scalar($expectedValue) && is_scalar($actualValue)) {
326-
if ($expectedValue !== $actualValue) {
327-
throw new ComparisonFailure(
328-
$expectedValue,
329-
$actualValue,
330-
'',
331-
'',
332-
false,
333-
sprintf('Field path "%s": %s', $keyPrefix . $key, 'Failed asserting that two values are equal.')
334-
);
335-
}
336-
337-
continue;
338-
}
339-
340316
$expectedType = get_debug_type($expectedValue);
341317
$actualType = get_debug_type($actualValue);
342318

343-
// Workaround for ObjectComparator printing the whole actual object
344-
if ($expectedType !== $actualType) {
319+
/* Early check to work around ObjectComparator printing the entire value
320+
* for a failed type comparison. Avoid doing this if either value is
321+
* numeric to allow for flexible numeric comparisons (e.g. 1 == 1.0). */
322+
if ($expectedType !== $actualType && ! (self::isNumeric($expectedValue) || self::isNumeric($actualValue))) {
345323
throw new ComparisonFailure(
346324
$expectedValue,
347325
$actualValue,
@@ -421,6 +399,11 @@ private function doToString()
421399
return 'matches ' . $this->exporter()->export($this->value);
422400
}
423401

402+
private static function isNumeric($value): bool
403+
{
404+
return is_int($value) || is_float($value) || $value instanceof Int64;
405+
}
406+
424407
/**
425408
* Prepare a BSON document or array for comparison.
426409
*

tests/SpecTests/DocumentsMatchConstraintTest.php

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public function testIgnoreExtraKeysInRoot(): void
4343
$this->assertResult(false, $c, [1, ['a' => 1, 'b' => 2]], 'Extra keys in embedded are not permitted');
4444
}
4545

46+
public function testFlexibleNumericComparison(): void
47+
{
48+
$c = new DocumentsMatchConstraint(['x' => 1, 'y' => 1.0]);
49+
$this->assertResult(true, $c, ['x' => 1.0, 'y' => 1.0], 'Float instead of expected int matches');
50+
$this->assertResult(true, $c, ['x' => 1, 'y' => 1], 'Int instead of expected float matches');
51+
$this->assertResult(false, $c, ['x' => 'foo', 'y' => 1.0], 'Different type does not match');
52+
}
53+
4654
public function testIgnoreExtraKeysInEmbedded(): void
4755
{
4856
$c = new DocumentsMatchConstraint(['x' => 1, 'y' => ['a' => 1, 'b' => 2]], false, true);
@@ -64,15 +72,6 @@ public function testIgnoreExtraKeysInEmbedded(): void
6472
$this->assertResult(false, $c, [1, ['a' => 2]], 'Keys must have the correct value');
6573
}
6674

67-
public function testPlaceholders(): void
68-
{
69-
$c = new DocumentsMatchConstraint(['x' => '42', 'y' => 42, 'z' => ['a' => 24]], false, false, [24, 42]);
70-
71-
$this->assertResult(true, $c, ['x' => '42', 'y' => 'foo', 'z' => ['a' => 1]], 'Placeholders accept any value');
72-
$this->assertResult(false, $c, ['x' => 42, 'y' => 'foo', 'z' => ['a' => 1]], 'Placeholder type must match');
73-
$this->assertResult(true, $c, ['x' => '42', 'y' => 42, 'z' => ['a' => 24]], 'Exact match');
74-
}
75-
7675
/**
7776
* @dataProvider provideBSONTypes
7877
*/
@@ -146,13 +145,13 @@ public function errorMessageProvider()
146145
['foo' => ['foo' => 'bar', 'bar' => 'baz']],
147146
],
148147
'Scalar value not equal' => [
149-
'Field path "foo": Failed asserting that two values are equal.',
148+
'Field path "foo": Failed asserting that two strings are equal.',
150149
new DocumentsMatchConstraint(['foo' => 'bar']),
151150
['foo' => 'baz'],
152151
],
153152
'Scalar type mismatch' => [
154-
'Field path "foo": Failed asserting that two values are equal.',
155-
new DocumentsMatchConstraint(['foo' => 42]),
153+
'Field path "foo": \'42\' is not instance of expected type "bool".',
154+
new DocumentsMatchConstraint(['foo' => true]),
156155
['foo' => '42'],
157156
],
158157
'Type mismatch' => [

tests/SpecTests/Operation.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use MongoDB\Driver\Server;
1313
use MongoDB\Driver\Session;
1414
use MongoDB\GridFS\Bucket;
15+
use MongoDB\MapReduceResult;
1516
use MongoDB\Model\IndexInfo;
1617
use MongoDB\Operation\FindOneAndReplace;
1718
use MongoDB\Operation\FindOneAndUpdate;
@@ -206,6 +207,10 @@ public function assert(FunctionalTestCase $test, Context $context, bool $bubbleE
206207
* is not used (e.g. Command Monitoring spec). */
207208
if ($result instanceof Cursor) {
208209
$result = $result->toArray();
210+
} elseif ($result instanceof MapReduceResult) {
211+
/* For mapReduce operations, we ignore the mapReduce metadata
212+
* and only return the result iterator for evaluation. */
213+
$result = iterator_to_array($result->getIterator());
209214
}
210215
} catch (Exception $e) {
211216
$exception = $e;
@@ -789,7 +794,7 @@ private function getResultAssertionTypeForCollection()
789794
return ResultExpectation::ASSERT_SAME_DOCUMENTS;
790795

791796
case 'mapReduce':
792-
return ResultExpectation::ASSERT_SAME_DOCUMENTS;
797+
return ResultExpectation::ASSERT_DOCUMENTS_MATCH;
793798

794799
case 'replaceOne':
795800
case 'updateMany':

tests/SpecTests/RetryableReadsSpecTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class RetryableReadsSpecTest extends FunctionalTestCase
2626
];
2727

2828
/** @var array */
29-
private static $incompleteTests = ['mapReduce: MapReduce succeeds with retry on' => 'PHPLIB-715'];
29+
private static $incompleteTests = [];
3030

3131
/**
3232
* Assert that the expected and actual command documents match.

tests/UnifiedSpecTests/Constraint/MatchesTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public function testMatchesDocument(): void
2323
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded key order is not significant');
2424
}
2525

26+
public function testFlexibleNumericComparison(): void
27+
{
28+
$c = new Matches(['x' => 1, 'y' => 1.0]);
29+
$this->assertResult(true, $c, ['x' => 1.0, 'y' => 1.0], 'Float instead of expected int matches');
30+
$this->assertResult(true, $c, ['x' => 1, 'y' => 1], 'Int instead of expected float matches');
31+
$this->assertResult(false, $c, ['x' => 'foo', 'y' => 1.0], 'Different type does not match');
32+
}
33+
2634
public function testDoNotAllowExtraRootKeys(): void
2735
{
2836
$c = new Matches(['x' => 1], null, false);

0 commit comments

Comments
 (0)