Skip to content

Commit 4608a73

Browse files
jmikolaalcaeus
andauthored
PHPLIB-493: Unified test runner POC (#783)
* Reduce isShardedCluster to a one-liner * wip * wip * wip * wip * Clean up constraints and add test coverage * Code fixes and fall back to assertInternalType for PHPUnit 6.x * phpcs fixes * Use Matches for CollectionData and validate operator syntax * Fix var usage * Fix static method call and allow empty string in $$matchesHexBytes * CS fixes * Include PHPUnit functions via autoload-dev * CR feedback * IsBsonType helpers and new IsStream constraint * Changes to get CRUD POC tests running * Implement expectEvents and change stream tests * Implement GridFS, transactions, and collate events by client * Remove stream entities and update GridFS operations * Todo items for Matches constraint * Assert basic structure of test files in data provider * Use assertion for testFailingTests * Implement session tests * Consider PHPUnit Warnings for expectError in GridFS tests * Disable fail points after tests * Fix targetedFailPoint operation * Fix MatchesTest and make EntityMap optional * Note cycling references and killAllSessions before each test * Sync unified spec tests * Fix phpcs validation * assertHasOnlyKeys requires array or stdClass * Add missing valid-fail test * Fix phpcs error * Remove nullable return type hint for PHP 7.0 * Update session test * Sync returnDocument-enum-invalid.json * Handle issues from code review * fix phpcs * fix tests * Fix wrong continue statement * Handle readPreferenceTags in URI options Co-authored-by: Andreas Braun <git@alcaeus.org>
1 parent 10b578f commit 4608a73

34 files changed

+7959
-7
lines changed

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"jean85/pretty-package-versions": "^1.2"
1717
},
1818
"require-dev": {
19-
"phpunit/phpunit": "^6.4",
19+
"phpunit/phpunit": "^6.5",
2020
"sebastian/comparator": "^2.0 || ^3.0",
2121
"squizlabs/php_codesniffer": "^3.5, <3.5.5",
2222
"symfony/phpunit-bridge": "^4.4@dev"
@@ -26,7 +26,10 @@
2626
"files": [ "src/functions.php" ]
2727
},
2828
"autoload-dev": {
29-
"psr-4": { "MongoDB\\Tests\\": "tests/" }
29+
"psr-4": { "MongoDB\\Tests\\": "tests/" },
30+
"// Manually include assertion functions for PHPUnit 8.x and earlier ":"",
31+
"// See: https://github.com/sebastianbergmann/phpunit/issues/3746 ":"",
32+
"files": [ "vendor/phpunit/phpunit/src/Framework/Assert/Functions.php" ]
3033
},
3134
"extra": {
3235
"branch-alias": {

tests/FunctionalTestCase.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,7 @@ protected function isReplicaSet()
308308

309309
protected function isShardedCluster()
310310
{
311-
if ($this->getPrimaryServer()->getType() == Server::TYPE_MONGOS) {
312-
return true;
313-
}
314-
315-
return false;
311+
return $this->getPrimaryServer()->getType() == Server::TYPE_MONGOS;
316312
}
317313

318314
protected function isShardedClusterUsingReplicasets()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\UnifiedSpecTests;
4+
5+
use ArrayIterator;
6+
use IteratorIterator;
7+
use MongoDB\Client;
8+
use MongoDB\Driver\ReadConcern;
9+
use MongoDB\Driver\ReadPreference;
10+
use MongoDB\Driver\WriteConcern;
11+
use MongoDB\Tests\UnifiedSpecTests\Constraint\Matches;
12+
use MultipleIterator;
13+
use stdClass;
14+
use function assertContainsOnly;
15+
use function assertInternalType;
16+
use function assertNotNull;
17+
use function assertThat;
18+
use function sprintf;
19+
20+
class CollectionData
21+
{
22+
/** @var string */
23+
private $collectionName;
24+
25+
/** @var string */
26+
private $databaseName;
27+
28+
/** @var array */
29+
private $documents;
30+
31+
public function __construct(stdClass $o)
32+
{
33+
assertInternalType('string', $o->collectionName);
34+
$this->collectionName = $o->collectionName;
35+
36+
assertInternalType('string', $o->databaseName);
37+
$this->databaseName = $o->databaseName;
38+
39+
assertInternalType('array', $o->documents);
40+
assertContainsOnly('object', $o->documents);
41+
$this->documents = $o->documents;
42+
}
43+
44+
public function prepareInitialData(Client $client)
45+
{
46+
$database = $client->selectDatabase(
47+
$this->databaseName,
48+
['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]
49+
);
50+
51+
$database->dropCollection($this->collectionName);
52+
53+
if (empty($this->documents)) {
54+
$database->createCollection($this->collectionName);
55+
56+
return;
57+
}
58+
59+
$database->selectCollection($this->collectionName)->insertMany($this->documents);
60+
}
61+
62+
public function assertOutcome(Client $client)
63+
{
64+
$collection = $client->selectCollection(
65+
$this->databaseName,
66+
$this->collectionName,
67+
[
68+
'readConcern' => new ReadConcern(ReadConcern::LOCAL),
69+
'readPreference' => new ReadPreference(ReadPreference::PRIMARY),
70+
]
71+
);
72+
73+
$cursor = $collection->find([], ['sort' => ['_id' => 1]]);
74+
75+
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
76+
$mi->attachIterator(new ArrayIterator($this->documents));
77+
$mi->attachIterator(new IteratorIterator($cursor));
78+
79+
foreach ($mi as $i => $documents) {
80+
list($expectedDocument, $actualDocument) = $documents;
81+
assertNotNull($expectedDocument);
82+
assertNotNull($actualDocument);
83+
84+
/* Prohibit extra root keys and disable operators to enforce exact
85+
* matching of documents. Key order variation is still allowed. */
86+
$constraint = new Matches($expectedDocument, null, false, false);
87+
assertThat($actualDocument, $constraint, sprintf('documents[%d] match', $i));
88+
}
89+
}
90+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\UnifiedSpecTests\Constraint;
4+
5+
use LogicException;
6+
use MongoDB\BSON\BinaryInterface;
7+
use MongoDB\BSON\DBPointer;
8+
use MongoDB\BSON\Decimal128Interface;
9+
use MongoDB\BSON\Int64;
10+
use MongoDB\BSON\JavascriptInterface;
11+
use MongoDB\BSON\MaxKeyInterface;
12+
use MongoDB\BSON\MinKeyInterface;
13+
use MongoDB\BSON\ObjectIdInterface;
14+
use MongoDB\BSON\RegexInterface;
15+
use MongoDB\BSON\Serializable;
16+
use MongoDB\BSON\Symbol;
17+
use MongoDB\BSON\TimestampInterface;
18+
use MongoDB\BSON\Type;
19+
use MongoDB\BSON\Undefined;
20+
use MongoDB\BSON\UTCDateTimeInterface;
21+
use MongoDB\Model\BSONArray;
22+
use MongoDB\Model\BSONDocument;
23+
use PHPUnit\Framework\Constraint\Constraint;
24+
use PHPUnit\Framework\Constraint\LogicalOr;
25+
use RuntimeException;
26+
use Symfony\Bridge\PhpUnit\ConstraintTrait;
27+
use function array_keys;
28+
use function array_map;
29+
use function count;
30+
use function in_array;
31+
use function is_array;
32+
use function is_bool;
33+
use function is_float;
34+
use function is_int;
35+
use function is_object;
36+
use function is_string;
37+
use function range;
38+
use function sprintf;
39+
use const PHP_INT_SIZE;
40+
41+
final class IsBsonType extends Constraint
42+
{
43+
use ConstraintTrait;
44+
45+
/** @var array */
46+
private static $types = [
47+
'double',
48+
'string',
49+
'object',
50+
'array',
51+
'binData',
52+
'undefined',
53+
'objectId',
54+
'bool',
55+
'date',
56+
'null',
57+
'regex',
58+
'dbPointer',
59+
'javascript',
60+
'symbol',
61+
'javascriptWithScope',
62+
'int',
63+
'timestamp',
64+
'long',
65+
'decimal',
66+
'minKey',
67+
'maxKey',
68+
];
69+
70+
/** @var string */
71+
private $type;
72+
73+
public function __construct(string $type)
74+
{
75+
if (! in_array($type, self::$types)) {
76+
throw new RuntimeException(sprintf('Type specified for %s <%s> is not a valid type', self::class, $type));
77+
}
78+
79+
$this->type = $type;
80+
}
81+
82+
public static function any() : LogicalOr
83+
{
84+
return self::anyOf(...self::$types);
85+
}
86+
87+
public static function anyOf(string ...$types) : Constraint
88+
{
89+
if (count($types) === 1) {
90+
return new self(...$types);
91+
}
92+
93+
return LogicalOr::fromConstraints(...array_map(function ($type) {
94+
return new self($type);
95+
}, $types));
96+
}
97+
98+
private function doMatches($other) : bool
99+
{
100+
switch ($this->type) {
101+
case 'double':
102+
return is_float($other);
103+
case 'string':
104+
return is_string($other);
105+
case 'object':
106+
return self::isObject($other);
107+
case 'array':
108+
return self::isArray($other);
109+
case 'binData':
110+
return $other instanceof BinaryInterface;
111+
case 'undefined':
112+
return $other instanceof Undefined;
113+
case 'objectId':
114+
return $other instanceof ObjectIdInterface;
115+
case 'bool':
116+
return is_bool($other);
117+
case 'date':
118+
return $other instanceof UTCDateTimeInterface;
119+
case 'null':
120+
return $other === null;
121+
case 'regex':
122+
return $other instanceof RegexInterface;
123+
case 'dbPointer':
124+
return $other instanceof DBPointer;
125+
case 'javascript':
126+
return $other instanceof JavascriptInterface && $other->getScope() === null;
127+
case 'symbol':
128+
return $other instanceof Symbol;
129+
case 'javascriptWithScope':
130+
return $other instanceof JavascriptInterface && $other->getScope() !== null;
131+
case 'int':
132+
return is_int($other);
133+
case 'timestamp':
134+
return $other instanceof TimestampInterface;
135+
case 'long':
136+
if (PHP_INT_SIZE == 4) {
137+
return $other instanceof Int64;
138+
}
139+
140+
return is_int($other);
141+
case 'decimal':
142+
return $other instanceof Decimal128Interface;
143+
case 'minKey':
144+
return $other instanceof MinKeyInterface;
145+
case 'maxKey':
146+
return $other instanceof MaxKeyInterface;
147+
default:
148+
// This should already have been caught in the constructor
149+
throw new LogicException('Unsupported type: ' . $this->type);
150+
}
151+
}
152+
153+
private function doToString() : string
154+
{
155+
return sprintf('is of BSON type "%s"', $this->type);
156+
}
157+
158+
private static function isArray($other) : bool
159+
{
160+
if ($other instanceof BSONArray) {
161+
return true;
162+
}
163+
164+
// Serializable can produce an array or object, so recurse on its output
165+
if ($other instanceof Serializable) {
166+
return self::isArray($other->bsonSerialize());
167+
}
168+
169+
if (! is_array($other)) {
170+
return false;
171+
}
172+
173+
// Empty and indexed arrays serialize as BSON arrays
174+
return self::isArrayEmptyOrIndexed($other);
175+
}
176+
177+
private static function isObject($other) : bool
178+
{
179+
if ($other instanceof BSONDocument) {
180+
return true;
181+
}
182+
183+
// Serializable can produce an array or object, so recurse on its output
184+
if ($other instanceof Serializable) {
185+
return self::isObject($other->bsonSerialize());
186+
}
187+
188+
// Non-empty, associative arrays serialize as BSON objects
189+
if (is_array($other)) {
190+
return ! self::isArrayEmptyOrIndexed($other);
191+
}
192+
193+
if (! is_object($other)) {
194+
return false;
195+
}
196+
197+
/* Serializable has already been handled, so any remaining instances of
198+
* Type will not serialize as BSON objects */
199+
return ! $other instanceof Type;
200+
}
201+
202+
private static function isArrayEmptyOrIndexed(array $a) : bool
203+
{
204+
if (empty($a)) {
205+
return true;
206+
}
207+
208+
return array_keys($a) === range(0, count($a) - 1);
209+
}
210+
}

0 commit comments

Comments
 (0)