Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix system test under Datastore Emulator. (Fixes #118) #119

Merged
merged 3 commits into from
Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/google/cloud/ndb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import contextlib
import os
import requests

from google.cloud import environment_vars
from google.cloud import _helpers
Expand Down Expand Up @@ -80,7 +81,6 @@ class Client(google_client.ClientWithProject):
"""The scopes required for authenticating as a Cloud Datastore consumer."""

def __init__(self, project=None, namespace=None, credentials=None):
super(Client, self).__init__(project=project, credentials=credentials)
self.namespace = namespace
self.host = os.environ.get(
environment_vars.GCD_HOST, DATASTORE_API_HOST
Expand All @@ -91,6 +91,22 @@ def __init__(self, project=None, namespace=None, credentials=None):
emulator = bool(os.environ.get("DATASTORE_EMULATOR_HOST"))
self.secure = not emulator

if emulator:
# When using the emulator, in theory, the client shouldn't need to
# call home to authenticate, as you don't need to authenticate to
# use the local emulator. Unfortunately, the client calls home to
# authenticate anyway, unless you pass ``requests.Session`` to
# ``_http`` which seems to be the preferred work around.
super(Client, self).__init__(
project=project,
credentials=credentials,
_http=requests.Session,
)
else:
super(Client, self).__init__(
project=project, credentials=credentials
)

@contextlib.contextmanager
def context(self, cache_policy=None):
"""Establish a context for a set of NDB calls.
Expand Down
2 changes: 1 addition & 1 deletion src/google/cloud/ndb/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,7 +2279,7 @@ def fetch_page_async(
results.append(result.entity())
cursor = result.cursor

more = (
more = results and (
iterator._more_results_after_limit or iterator.probably_has_next()
)
return results, cursor, more
Expand Down
39 changes: 27 additions & 12 deletions tests/system/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import itertools
import os
import uuid

import pytest
import requests

from google.cloud import datastore
from google.cloud import ndb

from . import KIND, OTHER_KIND, OTHER_NAMESPACE


def _make_ds_client(namespace):
emulator = bool(os.environ.get("DATASTORE_EMULATOR_HOST"))
if emulator:
client = datastore.Client(namespace=namespace, _http=requests.Session)
else:
client = datastore.Client(namespace=namespace)

return client


def all_entities(client):
return itertools.chain(
client.query(kind=KIND).fetch(),
Expand All @@ -20,7 +32,7 @@ def all_entities(client):
@pytest.fixture(scope="module", autouse=True)
def initial_clean():
# Make sure database is in clean state at beginning of test run
client = datastore.Client()
client = _make_ds_client(None)
for entity in all_entities(client):
client.delete(entity.key)

Expand All @@ -36,39 +48,42 @@ def to_delete():


@pytest.fixture
def ds_client(namespace, to_delete, deleted_keys):
client = datastore.Client(namespace=namespace)
def ds_client(namespace):
return _make_ds_client(namespace)


@pytest.fixture
def with_ds_client(ds_client, to_delete, deleted_keys):
# Make sure we're leaving database as clean as we found it after each test
results = [
entity
for entity in all_entities(client)
for entity in all_entities(ds_client)
if entity.key not in deleted_keys
]
assert not results

yield client
yield ds_client

if to_delete:
client.delete_multi(to_delete)
ds_client.delete_multi(to_delete)
deleted_keys.update(to_delete)

not_deleted = [
entity
for entity in all_entities(client)
for entity in all_entities(ds_client)
if entity.key not in deleted_keys
]
assert not not_deleted


@pytest.fixture
def ds_entity(ds_client, dispose_of):
def ds_entity(with_ds_client, dispose_of):
def make_entity(*key_args, **entity_kwargs):
key = ds_client.key(*key_args)
assert ds_client.get(key) is None
key = with_ds_client.key(*key_args)
assert with_ds_client.get(key) is None
entity = datastore.Entity(key=key)
entity.update(entity_kwargs)
ds_client.put(entity)
with_ds_client.put(entity)
dispose_of(key)

return entity
Expand All @@ -77,7 +92,7 @@ def make_entity(*key_args, **entity_kwargs):


@pytest.fixture
def dispose_of(ds_client, to_delete):
def dispose_of(with_ds_client, to_delete):
def delete_entity(ds_key):
to_delete.append(ds_key)

Expand Down
4 changes: 1 addition & 3 deletions tests/system/test_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import test_utils.system

from google.cloud import datastore
from google.cloud import ndb

from tests.system import KIND, eventually
Expand Down Expand Up @@ -132,7 +131,7 @@ def get_two_entities():


@pytest.mark.usefixtures("client_context")
def test_insert_entity(dispose_of):
def test_insert_entity(dispose_of, ds_client):
class SomeKind(ndb.Model):
foo = ndb.IntegerProperty()
bar = ndb.StringProperty()
Expand All @@ -145,7 +144,6 @@ class SomeKind(ndb.Model):
assert retrieved.bar == "none"

# Make sure strings are stored as strings in datastore
ds_client = datastore.Client()
ds_entity = ds_client.get(key._key)
assert ds_entity["bar"] == "none"

Expand Down
3 changes: 3 additions & 0 deletions tests/system/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ def make_entities():
page_size, start_cursor=next_cursor
)
assert [entity.foo for entity in results] == [5, 6, 7, 8, 9]

results, cursor, more = query.fetch_page(page_size, start_cursor=cursor)
assert not results
assert not more


Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,33 @@ def next(self):
raw=True,
)

@staticmethod
@pytest.mark.usefixtures("in_context")
@unittest.mock.patch("google.cloud.ndb.query._datastore_query")
def test_fetch_page_beyond_last_page(_datastore_query):
class DummyQueryIterator:
# Emulates the Datastore emulator behavior
_more_results_after_limit = True

def __init__(self):
self.items = []

def has_next_async(self):
return utils.future_result(False)

_datastore_query.iterate.return_value = DummyQueryIterator()
query = query_module.Query()
results, cursor, more = query.fetch_page(5, start_cursor="cursor000")
assert results == []
assert not more

_datastore_query.iterate.assert_called_once_with(
query_module.QueryOptions(
project="testing", limit=5, start_cursor="cursor000"
),
raw=True,
)

@staticmethod
@pytest.mark.usefixtures("in_context")
@unittest.mock.patch("google.cloud.ndb.query._datastore_query")
Expand Down