Skip to content

Commit

Permalink
PHPLIB-1569: Implement $$matchAsDocument and $$matchAsRoot (#1508)
Browse files Browse the repository at this point in the history
* PHPLIB-1569: Implement $$matchAsDocument and $$matchAsRoot

* PHPLIB-1577: Test for liberal numeric comparisons in Matches

* Update skip path for $$lte test

* Update specs submodule

* Skip distinct-hint tests pending PHPLIB-1582
  • Loading branch information
jmikola authored Nov 6, 2024
1 parent 21f5e44 commit 2bc2c91
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 3 deletions.
34 changes: 34 additions & 0 deletions tests/UnifiedSpecTests/Constraint/Matches.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace MongoDB\Tests\UnifiedSpecTests\Constraint;

use LogicException;
use MongoDB\BSON\Document;
use MongoDB\BSON\Serializable;
use MongoDB\BSON\Type;
use MongoDB\Model\BSONArray;
Expand All @@ -25,10 +26,13 @@
use function is_int;
use function is_object;
use function ltrim;
use function PHPUnit\Framework\assertInstanceOf;
use function PHPUnit\Framework\assertIsBool;
use function PHPUnit\Framework\assertIsString;
use function PHPUnit\Framework\assertJson;
use function PHPUnit\Framework\assertMatchesRegularExpression;
use function PHPUnit\Framework\assertNotNull;
use function PHPUnit\Framework\assertStringStartsWith;
use function PHPUnit\Framework\assertThat;
use function PHPUnit\Framework\containsOnly;
use function PHPUnit\Framework\isInstanceOf;
Expand All @@ -39,6 +43,7 @@
use function sprintf;
use function str_starts_with;
use function strrchr;
use function trim;

/**
* Constraint that checks if one value matches another.
Expand Down Expand Up @@ -263,6 +268,35 @@ private function assertMatchesOperator(BSONDocument $operator, $actual, string $
return;
}

if ($name === '$$matchAsDocument') {
assertInstanceOf(BSONDocument::class, $operator['$$matchAsDocument'], '$$matchAsDocument requires a BSON document');
assertIsString($actual, '$$matchAsDocument requires actual value to be a JSON string');
assertJson($actual, '$$matchAsDocument requires actual value to be a JSON string');

/* Note: assertJson() accepts array and scalar values, but the spec
* assumes that the JSON string will yield a document. */
assertStringStartsWith('{', trim($actual), '$$matchAsDocument requires actual value to be a JSON string denoting an object');

$actualDocument = Document::fromJSON($actual)->toPHP();
$constraint = new Matches($operator['$$matchAsDocument'], $this->entityMap, allowExtraRootKeys: false);

if (! $constraint->evaluate($actualDocument, '', true)) {
self::failAt(sprintf('%s did not match: %s', (new Exporter())->shortenedExport($actual), $constraint->additionalFailureDescription(null)), $keyPath);
}

return;
}

if ($name === '$$matchAsRoot') {
$constraint = new Matches($operator['$$matchAsRoot'], $this->entityMap, allowExtraRootKeys: true);

if (! $constraint->evaluate($actual, '', true)) {
self::failAt(sprintf('$actual did not match as root-level document: %s', $constraint->additionalFailureDescription(null)), $keyPath);
}

return;
}

if ($name === '$$matchesEntity') {
assertNotNull($this->entityMap, '$$matchesEntity requires EntityMap');
assertIsString($operator['$$matchesEntity'], '$$matchesEntity requires string');
Expand Down
41 changes: 41 additions & 0 deletions tests/UnifiedSpecTests/Constraint/MatchesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public function testFlexibleNumericComparison(): void
$this->assertResult(true, $c, ['x' => 1.0, 'y' => 1.0], 'Float instead of expected int matches');
$this->assertResult(true, $c, ['x' => 1, 'y' => 1], 'Int instead of expected float matches');
$this->assertResult(false, $c, ['x' => 'foo', 'y' => 1.0], 'Different type does not match');

/* Matches uses PHPUnit's comparators, which follow PHP behavior. This
* is more liberal than the comparison logic called for by the unified
* test format. This test can be removed when PHPLIB-1577 is addressed.
*/
$this->assertResult(true, $c, ['x' => '1.0', 'y' => '1'], 'Numeric strings may match ints and floats');
}

public function testDoNotAllowExtraRootKeys(): void
Expand Down Expand Up @@ -171,6 +177,37 @@ public function testOperatorSessionLsid(): void
$this->assertResult(false, $c, ['x' => 1], 'session LSID does not match (embedded)');
}

public function testOperatorMatchAsDocument(): void
{
$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1]]]);
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches');
$this->assertResult(false, $c, ['json' => '{"x": 2}'], 'JSON document does not match');
$this->assertResult(false, $c, ['json' => '{"x": 1, "y": 2}'], 'JSON document cannot contain extra fields');

$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => 1.0]]]);
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (flexible numeric comparison)');

$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$exists' => true]]]]);
$this->assertResult(true, $c, ['json' => '{"x": 1}'], 'JSON document matches (special operators)');
$this->assertResult(false, $c, ['json' => '{"y": 1}'], 'JSON document does not match (special operators)');

$c = new Matches(['json' => ['$$matchAsDocument' => ['x' => ['$$type' => 'objectId']]]]);
$this->assertResult(true, $c, ['json' => '{"x": {"$oid": "57e193d7a9cc81b4027498b5"}}'], 'JSON document matches (extended JSON)');
$this->assertResult(false, $c, ['json' => '{"x": {"$numberDecimal": "1234.5"}}'], 'JSON document does not match (extended JSON)');
}

public function testOperatorMatchAsRoot(): void
{
$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => 2]]]);
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (allow extra fields)');
$this->assertResult(true, $c, ['x' => ['y' => 2.0, 'z' => 3.0]], 'Nested document matches (flexible numeric comparison)');
$this->assertResult(false, $c, ['x' => ['y' => 3, 'z' => 3]], 'Nested document does not match');

$c = new Matches(['x' => ['$$matchAsRoot' => ['y' => ['$$exists' => true]]]]);
$this->assertResult(true, $c, ['x' => ['y' => 2, 'z' => 3]], 'Nested document matches (special operators)');
$this->assertResult(false, $c, ['x' => ['z' => 3]], 'Nested document matches (special operators)');
}

#[DataProvider('errorMessageProvider')]
public function testErrorMessages($expectedMessageRegex, Matches $constraint, $actualValue): void
{
Expand Down Expand Up @@ -302,6 +339,10 @@ public static function operatorErrorMessageProvider()
'$$sessionLsid requires string',
new Matches(['x' => ['$$sessionLsid' => 1]], new EntityMap()),
],
'$$matchAsDocument type' => [
'$$matchAsDocument requires a BSON document',
new Matches(['x' => ['$$matchAsDocument' => 'foo']]),
],
];
}

Expand Down
3 changes: 2 additions & 1 deletion tests/UnifiedSpecTests/UnifiedSpecTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class UnifiedSpecTest extends FunctionalTestCase
'crud/replaceOne-sort' => 'Sort for replace operations is not supported (PHPLIB-1492)',
'crud/updateOne-sort' => 'Sort for update operations is not supported (PHPLIB-1492)',
'crud/bypassDocumentValidation' => 'bypassDocumentValidation is handled by libmongoc (PHPLIB-1576)',
'crud/distinct-hint' => 'Hint for distinct operations is not supported (PHPLIB-1582)',
];

/** @var array<string, string> */
Expand All @@ -60,7 +61,7 @@ class UnifiedSpecTest extends FunctionalTestCase
'valid-pass/expectedEventsForClient-eventType: eventType defaults to command if unset' => 'PHPC does not implement CMAP',
// CSOT is not yet implemented (PHPC-1760)
'valid-pass/collectionData-createOptions: collection is created with the correct options' => 'CSOT is not yet implemented (PHPC-1760)',
'valid-pass/matches-lte-operator: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)',
'valid-pass/operator-lte: special lte matching operator' => 'CSOT is not yet implemented (PHPC-1760)',
// libmongoc always adds readConcern to aggregate command
'index-management/search index operations ignore read and write concern: listSearchIndexes ignores read and write concern' => 'libmongoc appends readConcern to aggregate command',
// Uses an invalid object name
Expand Down
2 changes: 1 addition & 1 deletion tests/UnifiedSpecTests/UnifiedTestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ final class UnifiedTestRunner
* - 1.9: Only createEntities operation is implemented
* - 1.10: Not implemented
* - 1.11: Not implemented, but CMAP is not applicable
* - 1.13: Not implemented
* - 1.13: Only $$matchAsDocument and $$matchAsRoot is implemented
* - 1.14: Not implemented
*/
public const MAX_SCHEMA_VERSION = '1.15';
Expand Down
2 changes: 1 addition & 1 deletion tests/specifications
Submodule specifications updated 40 files
+5 −0 .pre-commit-config.yaml
+14 −8 scripts/check_links.py
+55 −0 scripts/check_md_html.py
+7 −6 scripts/generate_index.py
+65 −57 scripts/migrate_to_md.py
+16 −16 source/auth/auth.md
+2 −2 source/bson-corpus/bson-corpus.md
+1 −0 source/bson-corpus/tests/datetime.json
+6 −6 source/bson-decimal128/decimal128.md
+22 −23 source/client-side-encryption/client-side-encryption.md
+7 −7 source/client-side-encryption/tests/README.md
+4 −4 source/compression/OP_COMPRESSED.md
+1 −1 source/connection-monitoring-and-pooling/connection-monitoring-and-pooling.md
+12 −0 source/crud/crud.md
+139 −0 source/crud/tests/unified/distinct-hint.json
+73 −0 source/crud/tests/unified/distinct-hint.yml
+1 −1 source/crud/tests/unified/estimatedDocumentCount.json
+1 −1 source/crud/tests/unified/estimatedDocumentCount.yml
+1 −72 source/mongodb-handshake/handshake.md
+73 −1 source/mongodb-handshake/tests/README.md
+2 −2 source/retryable-reads/tests/unified/estimatedDocumentCount.json
+1 −1 source/retryable-reads/tests/unified/estimatedDocumentCount.yml
+3 −2 source/socks5-support/tests/README.md
+205 −0 source/unified-test-format/tests/valid-fail/operator-matchAsDocument.json
+88 −0 source/unified-test-format/tests/valid-fail/operator-matchAsDocument.yml
+67 −0 source/unified-test-format/tests/valid-fail/operator-matchAsRoot.json
+33 −0 source/unified-test-format/tests/valid-fail/operator-matchAsRoot.yml
+74 −0 source/unified-test-format/tests/valid-pass/expectedError-isClientError.json
+39 −0 source/unified-test-format/tests/valid-pass/expectedError-isClientError.yml
+10 −0 source/unified-test-format/tests/valid-pass/operation-empty_array.json
+7 −0 source/unified-test-format/tests/valid-pass/operation-empty_array.yml
+1 −1 source/unified-test-format/tests/valid-pass/operator-lte.json
+2 −2 source/unified-test-format/tests/valid-pass/operator-lte.yml
+124 −0 source/unified-test-format/tests/valid-pass/operator-matchAsDocument.json
+54 −0 source/unified-test-format/tests/valid-pass/operator-matchAsDocument.yml
+151 −0 source/unified-test-format/tests/valid-pass/operator-matchAsRoot.json
+64 −0 source/unified-test-format/tests/valid-pass/operator-matchAsRoot.yml
+174 −0 source/unified-test-format/tests/valid-pass/operator-type-number_alias.json
+61 −0 source/unified-test-format/tests/valid-pass/operator-type-number_alias.yml
+3 −2 source/unified-test-format/unified-test-format.md

0 comments on commit 2bc2c91

Please sign in to comment.