Skip to content

Commit b55dbcc

Browse files
committed
Spec tests for Client::bulkWrite()
1 parent 0b57f5a commit b55dbcc

File tree

6 files changed

+240
-19
lines changed

6 files changed

+240
-19
lines changed

tests/UnifiedSpecTests/Constraint/Matches.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use LogicException;
66
use MongoDB\BSON\Document;
7+
use MongoDB\BSON\PackedArray;
78
use MongoDB\BSON\Serializable;
89
use MongoDB\BSON\Type;
910
use MongoDB\Model\BSONArray;
@@ -446,8 +447,14 @@ private static function prepare(mixed $bson): mixed
446447
return self::prepare($bson->bsonSerialize());
447448
}
448449

449-
/* Serializable has already been handled, so any remaining instances of
450-
* Type will not serialize as BSON arrays or objects */
450+
// Recurse on the PHP representation of Document and PackedArray types
451+
if ($bson instanceof Document || $bson instanceof PackedArray) {
452+
return self::prepare($bson->toPHP());
453+
}
454+
455+
/* Serializable, Document, and PackedArray have already been handled.
456+
* Any remaining Type instances will not serialize as BSON arrays or
457+
* objects. */
451458
if ($bson instanceof Type) {
452459
return $bson;
453460
}

tests/UnifiedSpecTests/ExpectedError.php

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace MongoDB\Tests\UnifiedSpecTests;
44

5+
use MongoDB\Driver\Exception\BulkWriteCommandException;
56
use MongoDB\Driver\Exception\BulkWriteException;
67
use MongoDB\Driver\Exception\CommandException;
78
use MongoDB\Driver\Exception\ExecutionTimeoutException;
@@ -12,6 +13,7 @@
1213
use stdClass;
1314
use Throwable;
1415

16+
use function count;
1517
use function PHPUnit\Framework\assertArrayHasKey;
1618
use function PHPUnit\Framework\assertContainsOnly;
1719
use function PHPUnit\Framework\assertCount;
@@ -56,14 +58,18 @@ final class ExpectedError
5658

5759
private ?string $codeName = null;
5860

59-
private ?Matches $matchesResultDocument = null;
61+
private ?Matches $matchesErrorResponse = null;
6062

6163
private array $includedLabels = [];
6264

6365
private array $excludedLabels = [];
6466

6567
private ?ExpectedResult $expectedResult = null;
6668

69+
private ?array $writeErrors = null;
70+
71+
private ?array $writeConcernErrors = null;
72+
6773
public function __construct(?stdClass $o, EntityMap $entityMap)
6874
{
6975
if ($o === null) {
@@ -98,7 +104,7 @@ public function __construct(?stdClass $o, EntityMap $entityMap)
98104

99105
if (isset($o->errorResponse)) {
100106
assertIsObject($o->errorResponse);
101-
$this->matchesResultDocument = new Matches($o->errorResponse, $entityMap);
107+
$this->matchesErrorResponse = new Matches($o->errorResponse, $entityMap);
102108
}
103109

104110
if (isset($o->errorLabelsContain)) {
@@ -116,6 +122,24 @@ public function __construct(?stdClass $o, EntityMap $entityMap)
116122
if (property_exists($o, 'expectResult')) {
117123
$this->expectedResult = new ExpectedResult($o, $entityMap);
118124
}
125+
126+
if (isset($o->writeErrors)) {
127+
assertIsObject($o->writeErrors);
128+
assertContainsOnly('object', (array) $o->writeErrors);
129+
130+
foreach ($o->writeErrors as $i => $writeError) {
131+
$this->writeErrors[$i] = new Matches($writeError, $entityMap);
132+
}
133+
}
134+
135+
if (isset($o->writeConcernErrors)) {
136+
assertIsArray($o->writeConcernErrors);
137+
assertContainsOnly('object', $o->writeConcernErrors);
138+
139+
foreach ($o->writeConcernErrors as $i => $writeConcernError) {
140+
$this->writeConcernErrors[$i] = new Matches($writeConcernError, $entityMap);
141+
}
142+
}
119143
}
120144

121145
/**
@@ -155,15 +179,21 @@ public function assert(?Throwable $e = null): void
155179
$this->assertCodeName($e);
156180
}
157181

158-
if (isset($this->matchesResultDocument)) {
159-
assertThat($e, logicalOr(isInstanceOf(CommandException::class), isInstanceOf(BulkWriteException::class)));
182+
if (isset($this->matchesErrorResponse)) {
183+
assertThat($e, logicalOr(
184+
isInstanceOf(CommandException::class),
185+
isInstanceOf(BulkWriteException::class),
186+
isInstanceOf(BulkWriteCommandException::class),
187+
));
160188

161189
if ($e instanceof CommandException) {
162-
assertThat($e->getResultDocument(), $this->matchesResultDocument, 'CommandException result document matches');
190+
assertThat($e->getResultDocument(), $this->matchesErrorResponse, 'CommandException result document matches expected errorResponse');
191+
} elseif ($e instanceof BulkWriteCommandException) {
192+
assertThat($e->getErrorReply(), $this->matchesErrorResponse, 'BulkWriteCommandException error reply matches expected errorResponse');
163193
} elseif ($e instanceof BulkWriteException) {
164194
$writeErrors = $e->getWriteResult()->getErrorReplies();
165195
assertCount(1, $writeErrors);
166-
assertThat($writeErrors[0], $this->matchesResultDocument, 'BulkWriteException result document matches');
196+
assertThat($writeErrors[0], $this->matchesErrorResponse, 'BulkWriteException first error reply matches expected errorResponse');
167197
}
168198
}
169199

@@ -180,16 +210,34 @@ public function assert(?Throwable $e = null): void
180210
}
181211

182212
if (isset($this->expectedResult)) {
183-
assertInstanceOf(BulkWriteException::class, $e);
184-
$this->expectedResult->assert($e->getWriteResult());
213+
assertThat($e, logicalOr(
214+
isInstanceOf(BulkWriteException::class),
215+
isInstanceOf(BulkWriteCommandException::class),
216+
));
217+
218+
if ($e instanceof BulkWriteCommandException) {
219+
$this->expectedResult->assert($e->getPartialResult());
220+
} elseif ($e instanceof BulkWriteException) {
221+
$this->expectedResult->assert($e->getWriteResult());
222+
}
223+
}
224+
225+
if (isset($this->writeErrors)) {
226+
assertInstanceOf(BulkWriteCommandException::class, $e);
227+
$this->assertWriteErrors($e->getWriteErrors());
228+
}
229+
230+
if (isset($this->writeConcernErrors)) {
231+
assertInstanceOf(BulkWriteCommandException::class, $e);
232+
$this->assertWriteConcernErrors($e->getWriteConcernErrors());
185233
}
186234
}
187235

188236
private function assertIsClientError(Throwable $e): void
189237
{
190-
/* Note: BulkWriteException may proxy a previous exception. Unwrap it
191-
* to check the original error. */
192-
if ($e instanceof BulkWriteException && $e->getPrevious() !== null) {
238+
/* Note: BulkWriteException and BulkWriteCommandException may proxy a
239+
* previous exception. Unwrap it to check the original error. */
240+
if (($e instanceof BulkWriteException || $e instanceof BulkWriteCommandException) && $e->getPrevious() !== null) {
193241
$e = $e->getPrevious();
194242
}
195243

@@ -226,4 +274,47 @@ private function assertCodeName(ServerException $e): void
226274
assertObjectHasProperty('codeName', $result);
227275
assertSame($this->codeName, $result->codeName);
228276
}
277+
278+
private function assertWriteErrors(array $writeErrors): void
279+
{
280+
assertCount(count($this->writeErrors), $writeErrors);
281+
282+
foreach ($this->writeErrors as $i => $matchesWriteError) {
283+
assertArrayHasKey($i, $writeErrors);
284+
$writeError = $writeErrors[$i];
285+
286+
// Not required by the spec test, but asserts PHPC correctness
287+
assertSame((int) $i, $writeError->getIndex());
288+
289+
/* Convert the WriteError into a document for matching. These
290+
* field names are derived from the CRUD spec. */
291+
$writeErrorDocument = [
292+
'code' => $writeError->getCode(),
293+
'message' => $writeError->getMessage(),
294+
'details' => $writeError->getInfo(),
295+
];
296+
297+
assertThat($writeErrorDocument, $matchesWriteError);
298+
}
299+
}
300+
301+
private function assertWriteConcernErrors(array $writeConcernErrors): void
302+
{
303+
assertCount(count($this->writeConcernErrors), $writeConcernErrors);
304+
305+
foreach ($this->writeConcernErrors as $i => $matchesWriteConcernError) {
306+
assertArrayHasKey($i, $writeConcernErrors);
307+
$writeConcernError = $writeConcernErrors[$i];
308+
309+
/* Convert the WriteConcernError into a document for matching.
310+
* These field names are derived from the CRUD spec. */
311+
$writeConcernErrorDocument = [
312+
'code' => $writeConcernError->getCode(),
313+
'message' => $writeConcernError->getMessage(),
314+
'details' => $writeConcernError->getInfo(),
315+
];
316+
317+
assertThat($writeConcernErrorDocument, $matchesWriteConcernError);
318+
}
319+
}
229320
}

tests/UnifiedSpecTests/ExpectedResult.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
use MongoDB\BulkWriteResult;
66
use MongoDB\DeleteResult;
7+
use MongoDB\Driver\BulkWriteCommandResult;
78
use MongoDB\Driver\WriteResult;
89
use MongoDB\InsertManyResult;
910
use MongoDB\InsertOneResult;
1011
use MongoDB\Tests\UnifiedSpecTests\Constraint\Matches;
1112
use MongoDB\UpdateResult;
1213
use stdClass;
1314

15+
use function array_filter;
1416
use function is_object;
1517
use function PHPUnit\Framework\assertThat;
1618
use function property_exists;
@@ -57,6 +59,10 @@ private static function prepare($value)
5759
return $value;
5860
}
5961

62+
if ($value instanceof BulkWriteCommandResult) {
63+
return self::prepareBulkWriteCommandResult($value);
64+
}
65+
6066
if (
6167
$value instanceof BulkWriteResult ||
6268
$value instanceof WriteResult ||
@@ -71,7 +77,31 @@ private static function prepare($value)
7177
return $value;
7278
}
7379

74-
private static function prepareWriteResult($value)
80+
private static function prepareBulkWriteCommandResult(BulkWriteCommandResult $result): array
81+
{
82+
$retval = [
83+
'deletedCount' => $result->getDeletedCount(),
84+
'insertedCount' => $result->getInsertedCount(),
85+
'matchedCount' => $result->getMatchedCount(),
86+
'modifiedCount' => $result->getModifiedCount(),
87+
'upsertedCount' => $result->getUpsertedCount(),
88+
];
89+
90+
/* Tests use $$unsetOrMatches to expect either no key or an empty
91+
* document when verboseResults=false, so filter out null values. */
92+
$retval += array_filter(
93+
[
94+
'deleteResults' => $result->getDeleteResults()?->toPHP(),
95+
'insertResults' => $result->getInsertResults()?->toPHP(),
96+
'updateResults' => $result->getUpdateResults()?->toPHP(),
97+
],
98+
fn ($value) => $value !== null,
99+
);
100+
101+
return $retval;
102+
}
103+
104+
private static function prepareWriteResult($value): array
75105
{
76106
$result = ['acknowledged' => $value->isAcknowledged()];
77107

tests/UnifiedSpecTests/Operation.php

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use MongoDB\Client;
88
use MongoDB\Collection;
99
use MongoDB\Database;
10+
use MongoDB\Driver\BulkWriteCommand;
1011
use MongoDB\Driver\ClientEncryption;
1112
use MongoDB\Driver\Cursor;
1213
use MongoDB\Driver\Server;
@@ -87,10 +88,7 @@ final class Operation
8788
'assertNumberConnectionsCheckedOut' => 'PHP does not implement CMAP',
8889
'createEntities' => 'createEntities is not implemented (PHPC-1760)',
8990
],
90-
Client::class => [
91-
'clientBulkWrite' => 'clientBulkWrite is not implemented (PHPLIB-847)',
92-
'listDatabaseObjects' => 'listDatabaseObjects is not implemented',
93-
],
91+
Client::class => ['listDatabaseObjects' => 'listDatabaseObjects is not implemented'],
9492
Cursor::class => ['iterateOnce' => 'iterateOnce is not implemented (PHPC-1760)'],
9593
Database::class => [
9694
'createCommandCursor' => 'commandCursor API is not yet implemented (PHPLIB-1077)',
@@ -257,6 +255,18 @@ private function executeForClient(Client $client)
257255
Util::assertArgumentsBySchema(Client::class, $this->name, $args);
258256

259257
switch ($this->name) {
258+
case 'clientBulkWrite':
259+
assertArrayHasKey('models', $args);
260+
assertIsArray($args['models']);
261+
262+
// Options for BulkWriteCommand and Server::executeBulkWriteCommand() will be mixed
263+
$options = array_diff_key($args, ['models' => 1]);
264+
265+
return $client->bulkWrite(
266+
self::prepareBulkWriteCommand($args['models'], $options),
267+
$options,
268+
);
269+
260270
case 'createChangeStream':
261271
assertArrayHasKey('pipeline', $args);
262272
assertIsArray($args['pipeline']);
@@ -1001,6 +1011,82 @@ private function skipIfOperationIsNotSupported(string $executingObjectName): voi
10011011
Assert::markTestSkipped($skipReason);
10021012
}
10031013

1014+
private static function prepareBulkWriteCommand(array $models, array $options): BulkWriteCommand
1015+
{
1016+
$bulk = new BulkWriteCommand($options);
1017+
1018+
foreach ($models as $model) {
1019+
$model = (array) $model;
1020+
assertCount(1, $model);
1021+
1022+
$type = key($model);
1023+
$args = current($model);
1024+
assertIsObject($args);
1025+
$args = (array) $args;
1026+
1027+
assertArrayHasKey('namespace', $args);
1028+
assertIsString($args['namespace']);
1029+
1030+
switch ($type) {
1031+
case 'deleteMany':
1032+
case 'deleteOne':
1033+
assertArrayHasKey('filter', $args);
1034+
assertInstanceOf(stdClass::class, $args['filter']);
1035+
1036+
$bulk->{$type}(
1037+
$args['namespace'],
1038+
$args['filter'],
1039+
array_diff_key($args, ['namespace' => 1, 'filter' => 1]),
1040+
);
1041+
break;
1042+
1043+
case 'insertOne':
1044+
assertArrayHasKey('document', $args);
1045+
assertInstanceOf(stdClass::class, $args['document']);
1046+
1047+
$bulk->insertOne(
1048+
$args['namespace'],
1049+
$args['document'],
1050+
);
1051+
break;
1052+
1053+
case 'replaceOne':
1054+
assertArrayHasKey('filter', $args);
1055+
assertArrayHasKey('replacement', $args);
1056+
assertInstanceOf(stdClass::class, $args['filter']);
1057+
assertInstanceOf(stdClass::class, $args['replacement']);
1058+
1059+
$bulk->replaceOne(
1060+
$args['namespace'],
1061+
$args['filter'],
1062+
$args['replacement'],
1063+
array_diff_key($args, ['namespace' => 1, 'filter' => 1, 'replacement' => 1]),
1064+
);
1065+
break;
1066+
1067+
case 'updateMany':
1068+
case 'updateOne':
1069+
assertArrayHasKey('filter', $args);
1070+
assertArrayHasKey('update', $args);
1071+
assertInstanceOf(stdClass::class, $args['filter']);
1072+
assertThat($args['update'], logicalOr(new IsType('array'), new IsType('object')));
1073+
1074+
$bulk->{$type}(
1075+
$args['namespace'],
1076+
$args['filter'],
1077+
$args['update'],
1078+
array_diff_key($args, ['namespace' => 1, 'filter' => 1, 'update' => 1]),
1079+
);
1080+
break;
1081+
1082+
default:
1083+
Assert::fail('Unsupported bulk write model: ' . $type);
1084+
}
1085+
}
1086+
1087+
return $bulk;
1088+
}
1089+
10041090
private static function prepareBulkWriteRequest(stdClass $request): array
10051091
{
10061092
$request = (array) $request;
@@ -1026,6 +1112,7 @@ private static function prepareBulkWriteRequest(stdClass $request): array
10261112

10271113
case 'insertOne':
10281114
assertArrayHasKey('document', $args);
1115+
assertInstanceOf(stdClass::class, $args['document']);
10291116

10301117
return ['insertOne' => [$args['document']]];
10311118

0 commit comments

Comments
 (0)