Skip to content

Commit

Permalink
Add firestore conformance tests (#747)
Browse files Browse the repository at this point in the history
* Add firestore conformance tests

* Fix VarInt formatting

* Address code review
  • Loading branch information
jdpedrie authored Nov 16, 2017
1 parent 87105df commit 4c7f963
Show file tree
Hide file tree
Showing 30 changed files with 2,015 additions and 54 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
},
"files": [
"dev/src/Functions.php"
],
"classmap": [
"tests/conformance-fixtures"
]
},
"scripts": {
Expand Down
16 changes: 16 additions & 0 deletions phpunit-conformance.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./tests/conformance/bootstrap.php" colors="true">
<testsuites>
<testsuite>
<directory>tests/conformance</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
<exclude>
<directory suffix=".php">src/*/V[!a-zA-Z]*</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
6 changes: 0 additions & 6 deletions src/Firestore/Connection/Grpc.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,6 @@ public function commit(array $args)
{
$writes = $this->pluck('writes', $args);
foreach ($writes as $idx => $write) {
if (isset($write['updateMask'])) {
$maskFields = $write['updateMask'];
$write['updateMask'] = new DocumentMask;
$write['updateMask']->setFieldPaths($maskFields);
}

$writes[$idx] = $this->serializer->decodeMessage(new Write, $write);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Firestore/FieldPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class FieldPath
/**
* @param array $fieldNames A list of field names.
*/
public function __construct(array $fieldNames = [])
public function __construct(array $fieldNames)
{
$this->fieldNames = $fieldNames;
}
Expand Down
8 changes: 4 additions & 4 deletions src/Firestore/FirestoreClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,10 @@ public function documents(array $paths, array $options = [])
* @param array $options {
* Configuration Options.
*
* @param array $begin Configuration options for BeginTransaction.
* @param array $commit Configuration options for Commit.
* @param array $rollback Configuration options for rollback.
* @param int $maxRetries The maximum number of times to retry failures.
* @type array $begin Configuration options for BeginTransaction.
* @type array $commit Configuration options for Commit.
* @type array $rollback Configuration options for rollback.
* @type int $maxRetries The maximum number of times to retry failures.
* **Defaults to** `5`.
* }
* @return array
Expand Down
2 changes: 1 addition & 1 deletion src/Firestore/PathTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private function fullName($projectId, $database, $relativeName)
* @param string $relativeName
* @return string
*/
public function fullNameFromDatabase($databaseName, $relativeName)
private function fullNameFromDatabase($databaseName, $relativeName)
{
$template = '%s/documents/%s';
return sprintf($template, $databaseName, $relativeName);
Expand Down
1 change: 1 addition & 0 deletions src/Firestore/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Google\Cloud\Core\DebugInfoTrait;
use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\Firestore\Connection\ConnectionInterface;
use Google\Cloud\Firestore\DocumentSnapshot;
use Google\Cloud\Firestore\SnapshotTrait;
use Google\Firestore\V1beta1\StructuredQuery_CompositeFilter_Operator;
use Google\Firestore\V1beta1\StructuredQuery_Direction;
Expand Down
8 changes: 4 additions & 4 deletions src/Firestore/ValueMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public function escapeFieldPath($fieldPath)
}

$fieldPath = implode('.', $out);
} else {
if (!preg_match(self::VALID_FIELD_PATH, $fieldPath)) {
throw new \InvalidArgumentException('Paths cannot be empty and must not contain `*~/[]\`.');
}
}

$this->validateFieldPath($fieldPath);
Expand Down Expand Up @@ -495,9 +499,5 @@ private function validateFieldPath($fieldPath)
if (strpos($fieldPath, '.') === 0 || strpos(strrev($fieldPath), '.') === 0) {
throw new \InvalidArgumentException('Paths cannot begin or end with `.`.');
}

if (!preg_match(self::VALID_FIELD_PATH, $fieldPath)) {
throw new \InvalidArgumentException('Paths cannot be empty and must not contain `*~/[]\`.');
}
}
}
69 changes: 54 additions & 15 deletions src/Firestore/WriteBatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,32 @@ public function __construct(ConnectionInterface $connection, $valueMapper, $data
* field paths are NOT supported by this method.
* @param array $options Configuration options
* @return WriteBatch
* @throws \InvalidArgumentException If delete field sentinels are found in the fields list.
*/
public function create($documentName, array $fields, array $options = [])
{
$this->writes[] = $this->createDatabaseWrite(self::TYPE_UPDATE, $documentName, [
'fields' => $this->valueMapper->encodeValues($fields),
'precondition' => ['exists' => false]
] + $options);
list($fields, $timestamps, $deletes) = $this->valueMapper->findSentinels($fields);

if (!empty($deletes)) {
throw new \InvalidArgumentException('Cannot delete fields when creating a document.');
}

$precondition = ['exists' => false];

$transformOptions = [];
if (!empty($fields)) {
$this->writes[] = $this->createDatabaseWrite(self::TYPE_UPDATE, $documentName, [
'fields' => $this->valueMapper->encodeValues($fields),
'precondition' => $precondition
] + $options);
} else {
$transformOptions = [
'precondition' => $precondition
];
}

// Setting values to the server timestamp is implemented as a document tranformation.
$this->updateTransforms($documentName, $timestamps, $transformOptions);

return $this;
}
Expand Down Expand Up @@ -148,14 +167,13 @@ public function create($documentName, array $fields, array $options = [])
* }
* @return WriteBatch
* @codingStandardsIgnoreEnd
* @throws \InvalidArgumentException If the fields list is empty when `$options.merge` is `true`.
*/
public function set($documentName, array $fields, array $options = [])
{
$options += [
'merge' => false
];
$merge = $this->pluck('merge', $options, false) ?: false;

if ($options['merge'] && empty($fields)) {
if ($merge && empty($fields)) {
throw new \InvalidArgumentException('Fields list cannot be empty when merging fields.');
}

Expand All @@ -164,7 +182,7 @@ public function set($documentName, array $fields, array $options = [])
if ($fields) {
$write = array_filter([
'fields' => $this->valueMapper->encodeValues($fields),
'updateMask' => $options['merge'] ? $this->valueMapper->encodeFieldPaths($fields) : null
'updateMask' => $merge ? $this->valueMapper->encodeFieldPaths($fields) : null
]);

$this->writes[] = $this->createDatabaseWrite(self::TYPE_UPDATE, $documentName, $write);
Expand Down Expand Up @@ -228,15 +246,16 @@ public function set($documentName, array $fields, array $options = [])
* @param array[] $data A list of arrays of form `[FieldPath|string $path, mixed $value]`.
* @param array $options Configuration options
* @return WriteBatch
* @throws \InvalidArgumentException If data is given in an invalid format.
* @throws \InvalidArgumentException If data is given in an invalid format or is empty.
* @throws \InvalidArgumentException If any field paths are empty.
*/
public function update($documentName, array $data, array $options = [])
{
if (!empty($data) && $this->isAssoc($data)) {
if (!$data || $this->isAssoc($data)) {
throw new \InvalidArgumentException(
'Field data must be provided as a list of arrays of form `[string|FieldPath $path, mixed $value]`.'
);
} elseif (empty($data)) {
} elseif (!$data) {
throw new \InvalidArgumentException(
'Field data cannot be empty.'
);
Expand All @@ -251,17 +270,25 @@ public function update($documentName, array $data, array $options = [])
foreach ($data as $field) {
$this->arrayHasKeys($field, ['path', 'value']);

$paths[] = ($field['path'] instanceof FieldPath)
$path = ($field['path'] instanceof FieldPath)
? $field['path']
: FieldPath::fromString($field['path']);

if (!$path->path()) {
throw new \InvalidArgumentException('Field Path cannot be empty.');
}

$paths[] = $path;

$values[] = $field['value'];
}

$fields = $this->valueMapper->buildDocumentFromPathsAndValues($paths, $values);

list($fields, $timestamps, $deletes) = $this->valueMapper->findSentinels($fields);

$transformOptions = [];

// We only want to enqueue an update write if there are non-sentinel fields
// OR no timestamp sentinels are found.
// We MUST always enqueue at least one write, so if there are no fields
Expand All @@ -281,10 +308,14 @@ public function update($documentName, array $data, array $options = [])
'fields' => $this->valueMapper->encodeValues($fields),
'updateMask' => array_unique(array_merge($updateMask, $deletes))
] + $options);
} else {
$transformOptions = [
'precondition' => $options['precondition']
];
}

// Setting values to the server timestamp is implemented as a document tranformation.
$this->updateTransforms($documentName, $timestamps);
$this->updateTransforms($documentName, $timestamps, $transformOptions);

return $this;
}
Expand Down Expand Up @@ -327,6 +358,8 @@ public function delete($documentName, array $options = [])
*/
public function commit(array $options = [])
{
unset($options['merge'], $options['precondition']);

$response = $this->connection->commit(array_filter([
'database' => $this->database,
'writes' => $this->writes,
Expand Down Expand Up @@ -413,8 +446,14 @@ private function updateTransforms($documentName, array $timestamps, array $optio
*/
private function createDatabaseWrite($type, $name, array $options = [])
{
$mask = $this->pluck('updateMask', $options, false);
if ($mask) {
sort($mask);
$mask = ['fieldPaths' => $mask];
}

return array_filter([
'updateMask' => $this->pluck('updateMask', $options, false),
'updateMask' => $mask,
'currentDocument' => $this->validatePrecondition($options),
]) + $this->createDatabaseWriteOperation($type, $name, $options);
}
Expand Down
48 changes: 48 additions & 0 deletions tests/ArrayHasSameValuesToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Google\Cloud\Tests;

use Prophecy\Argument\Token\TokenInterface;
use Prophecy\Util\StringUtil;

class ArrayHasSameValuesToken implements TokenInterface
{
private $value;
private $string;
private $util;

public function __construct($value, StringUtil $util = null)
{
$this->value = $value;
$this->util = $util ?: new StringUtil();
}

public function scoreArgument($argument)
{
return $this->compare($this->value, $argument) ? 11 : false;
}

private function compare(array $value, array $argument)
{
array_multisort($value);
array_multisort($argument);

return $value == $argument;
}

public function isLast()
{
return false;
}

public function __toString()
{
if ($this->string) {
$string = $this->string .': (%s)';
} else {
$string = 'same(%s)';
}

return sprintf($string, $this->util->stringify($this->value));
}
}
73 changes: 73 additions & 0 deletions tests/conformance-fixtures/Firestore/GPBMetadata/Test.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4c7f963

Please sign in to comment.