Skip to content

Commit

Permalink
Adding support for queries in regression test.
Browse files Browse the repository at this point in the history
This is an attempt port the gcloud-node regression tests
for queries over to gcloud-python.

This surfaced some API differences (i.e. issue #280)
and some missing features (projection, offset and group_by).

In addition, __eq__ was implemented on datastore.key.Key
to allow for easy comparison within tests.
  • Loading branch information
dhermes committed Oct 22, 2014
1 parent 1ffe451 commit 99f00f3
Show file tree
Hide file tree
Showing 7 changed files with 510 additions and 19 deletions.
30 changes: 30 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ Running Regression Tests

$ python regression/run_regression.py --package {package}

This alone will not run the tests. You'll need to change some local
auth settings and change some configuration in your project to
run all the tests.

- Regression tests will be run against an actual project and
so you'll need to provide some environment variables to facilitate
authentication to your project:
Expand All @@ -149,6 +153,9 @@ Running Regression Tests
- ``GCLOUD_TESTS_CLIENT_EMAIL``: The email for the service account you're
authenticating with
- ``GCLOUD_TESTS_KEY_FILE``: The path to an encrypted key file.
See private key
`docs <https://cloud.google.com/storage/docs/authentication#generating-a-private-key>`__
for explanation on how to get a private key.

- Examples of these can be found in ``regression/local_test_setup.sample``. We
recommend copying this to ``regression/local_test_setup``, editing the values
Expand All @@ -160,6 +167,29 @@ Running Regression Tests
absolute) on your system where the key file for your service account can
be found.

- For datastore tests, you'll need to create composite
`indexes <https://cloud.google.com/datastore/docs/tools/indexconfig>`__
with the ``gcloud`` command line
`tool <https://developers.google.com/cloud/sdk/gcloud/>`__::

# Install the app (App Engine Command Line Interface) component.
$ gcloud components update app

# See https://cloud.google.com/sdk/crypto for details on PyOpenSSL and
# http://stackoverflow.com/a/25067729/1068170 for why we must persist.
$ export CLOUDSDK_PYTHON_SITEPACKAGES=1

# Authenticate the gcloud tool with your account.
$ gcloud auth activate-service-account $GCLOUD_TESTS_CLIENT_EMAIL \
> --key-file=$GCLOUD_TESTS_KEY_FILE

# Create the indexes
$ gcloud preview datastore create-indexes regression/data/ \
> --project=$GCLOUD_TESTS_DATASET_ID

# Restore your environment to its previous state.
$ unset CLOUDSDK_PYTHON_SITEPACKAGES

Test Coverage
-------------

Expand Down
11 changes: 11 additions & 0 deletions gcloud/datastore/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,14 @@ def parent(self):

def __repr__(self):
return '<Key%s>' % self.path()

def __eq__(self, other):
if self is other:
return True

return (self.dataset() == other.dataset() and
self.namespace() == other.namespace() and
self.path() == other.path())

def __ne__(self, other):
return not self.__eq__(other)
104 changes: 104 additions & 0 deletions gcloud/datastore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ def __init__(self, kind=None, dataset=None, namespace=None):
self._namespace = namespace
self._pb = datastore_pb.Query()
self._cursor = None
self._projection = []
self._offset = 0
self._group_by = []

if kind:
self._pb.kind.add().name = kind
Expand Down Expand Up @@ -404,3 +407,104 @@ def order(self, *properties):
property_order.direction = property_order.ASCENDING

return clone

def projection(self, projection=None):
"""Adds a projection to the query.
This is a hybrid getter / setter, used as::
>>> query = Query('Person')
>>> query.projection() # Get the projection for this query.
[]
>>> query = query.projection(['name'])
>>> query.projection() # Get the projection for this query.
['name']
:type projection: sequence of strings
:param projection: Each value is a string giving the name of a
property to be included in the projection query.
:rtype: :class:`Query` or `list` of strings.
:returns: If no arguments, returns the current projection.
If a projection is provided, returns a clone of the
:class:`Query` with that projection set.
"""
if projection is None:
return self._projection

clone = self._clone()
clone._projection = projection

# Reset projection values to empty.
clone._pb.projection._values = []

# Add each name to list of projections.
for projection_name in projection:
clone._pb.projection.add().property.name = projection_name
return clone

def offset(self, offset=None):
"""Adds offset to the query to allow pagination.
NOTE: Paging with cursors should be preferred to using an offset.
This is a hybrid getter / setter, used as::
>>> query = Query('Person')
>>> query.offset() # Get the offset for this query.
0
>>> query = query.offset(10)
>>> query.offset() # Get the offset for this query.
10
:type offset: non-negative integer.
:param offset: Value representing where to start a query for
a given kind.
:rtype: :class:`Query` or `int`.
:returns: If no arguments, returns the current offset.
If an offset is provided, returns a clone of the
:class:`Query` with that offset set.
"""
if offset is None:
return self._offset

clone = self._clone()
clone._offset = offset
clone._pb.offset = offset
return clone

def group_by(self, group_by=None):
"""Adds a group_by to the query.
This is a hybrid getter / setter, used as::
>>> query = Query('Person')
>>> query.group_by() # Get the group_by for this query.
[]
>>> query = query.group_by(['name'])
>>> query.group_by() # Get the group_by for this query.
['name']
:type group_by: sequence of strings
:param group_by: Each value is a string giving the name of a
property to use to group results together.
:rtype: :class:`Query` or `list` of strings.
:returns: If no arguments, returns the current group_by.
If a list of group by properties is provided, returns a clone
of the :class:`Query` with that list of values set.
"""
if group_by is None:
return self._group_by

clone = self._clone()
clone._group_by = group_by

# Reset group_by values to empty.
clone._pb.group_by._values = []

# Add each name to list of group_bys.
for group_by_name in group_by:
clone._pb.group_by.add().name = group_by_name
return clone
10 changes: 10 additions & 0 deletions gcloud/datastore/test_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,13 @@ def test_parent_explicit_top_level(self):
def test_parent_explicit_nested(self):
key = self._getTargetClass().from_path('abc', 'def', 'ghi', 123)
self.assertEqual(key.parent().path(), [{'kind': 'abc', 'name': 'def'}])

def test_key___eq__(self):
key1 = self._getTargetClass().from_path('abc', 'def')
key2 = self._getTargetClass().from_path('abc', 'def')
self.assertFalse(key1 is key2)
self.assertEqual(key1, key2)

self.assertEqual(key1, key1)
key3 = self._getTargetClass().from_path('abc', 'ghi')
self.assertNotEqual(key1, key3)
64 changes: 64 additions & 0 deletions gcloud/datastore/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,70 @@ def test_order_multiple(self):
self.assertEqual(prop_pb.property.name, 'bar')
self.assertEqual(prop_pb.direction, prop_pb.DESCENDING)

def test_projection_empty(self):
_KIND = 'KIND'
before = self._makeOne(_KIND)
after = before.projection([])
self.assertFalse(after is before)
self.assertTrue(isinstance(after, self._getTargetClass()))
self.assertEqual(before.to_protobuf(), after.to_protobuf())

def test_projection_non_empty(self):
_KIND = 'KIND'
before = self._makeOne(_KIND)
after = before.projection(['field1', 'field2'])
projection_pb = list(after.to_protobuf().projection)
self.assertEqual(len(projection_pb), 2)
prop_pb1 = projection_pb[0]
self.assertEqual(prop_pb1.property.name, 'field1')
prop_pb2 = projection_pb[1]
self.assertEqual(prop_pb2.property.name, 'field2')

def test_get_projection_non_empty(self):
_KIND = 'KIND'
_PROJECTION = ['field1', 'field2']
after = self._makeOne(_KIND).projection(_PROJECTION)
self.assertEqual(after.projection(), _PROJECTION)

def test_set_offset(self):
_KIND = 'KIND'
_OFFSET = 42
before = self._makeOne(_KIND)
after = before.offset(_OFFSET)
offset_pb = after.to_protobuf().offset
self.assertEqual(offset_pb, _OFFSET)

def test_get_offset(self):
_KIND = 'KIND'
_OFFSET = 10
after = self._makeOne(_KIND).offset(_OFFSET)
self.assertEqual(after.offset(), _OFFSET)

def test_group_by_empty(self):
_KIND = 'KIND'
before = self._makeOne(_KIND)
after = before.group_by([])
self.assertFalse(after is before)
self.assertTrue(isinstance(after, self._getTargetClass()))
self.assertEqual(before.to_protobuf(), after.to_protobuf())

def test_group_by_non_empty(self):
_KIND = 'KIND'
before = self._makeOne(_KIND)
after = before.group_by(['field1', 'field2'])
group_by_pb = list(after.to_protobuf().group_by)
self.assertEqual(len(group_by_pb), 2)
prop_pb1 = group_by_pb[0]
self.assertEqual(prop_pb1.name, 'field1')
prop_pb2 = group_by_pb[1]
self.assertEqual(prop_pb2.name, 'field2')

def test_get_group_by_non_empty(self):
_KIND = 'KIND'
_GROUP_BY = ['field1', 'field2']
after = self._makeOne(_KIND).group_by(_GROUP_BY)
self.assertEqual(after.group_by(), _GROUP_BY)


class _Dataset(object):

Expand Down
11 changes: 11 additions & 0 deletions regression/data/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
indexes:

- kind: Character
properties:
- name: family
- name: appearances

- kind: Character
properties:
- name: name
- name: family
Loading

0 comments on commit 99f00f3

Please sign in to comment.