Skip to content

Commit da431a2

Browse files
committed
Add unit test for spanner connection functionality
Adding regression tests for the issue that was fixed in PR google#5. As part of the testing, switched over many places that were detecting errors caused by improper library usage to better errors than AssertionError
1 parent fc4d692 commit da431a2

13 files changed

Lines changed: 275 additions & 128 deletions

File tree

spanner_orm/admin/api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
"""Class that handles API calls to Spanner that deal with table metadata."""
1616

1717
from spanner_orm import api
18+
from spanner_orm import error
1819

1920
from google.cloud import spanner
2021

2122

22-
class SpannerAdminApi(api.TableReadApi):
23+
class SpannerAdminApi(api.SpannerReadApi):
2324
"""Manages table schema information on Spanner."""
2425

2526
_connection_info = None
@@ -54,7 +55,8 @@ def connect(cls,
5455

5556
@classmethod
5657
def _connection(cls):
57-
assert cls._spanner_connection is not None
58+
if not cls._spanner_connection:
59+
raise error.SpannerError('Not connected to spanner')
5860
return cls._spanner_connection
5961

6062
@classmethod

spanner_orm/admin/metadata.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
# limitations under the License.
1515
"""Retrieves table metadata from Spanner."""
1616

17-
from collections import defaultdict
17+
import collections
1818

1919
from spanner_orm import condition
20+
from spanner_orm import error
2021
from spanner_orm import model
2122
from spanner_orm import update
2223
from spanner_orm.admin import api
@@ -30,24 +31,35 @@ class DatabaseMetadata(object):
3031

3132
@classmethod
3233
def column_update(cls, schema_change):
33-
assert isinstance(schema_change, update.ColumnUpdate)
34+
"""Handles an ALTER TABLE schema update."""
35+
if not isinstance(schema_change, update.ColumnUpdate):
36+
raise error.SpannerError(
37+
'column_update must be provided a ColumnUpdate')
3438
klass = cls.models()[schema_change.table()]
3539
schema_change.validate(klass)
3640

3741
api.SpannerAdminApi.update_schema(schema_change.ddl(klass))
3842

3943
@classmethod
4044
def create_table(cls, schema_change):
41-
assert isinstance(schema_change, update.CreateTableUpdate)
45+
"""Handles a CREATE TABLE schema update."""
46+
if not isinstance(schema_change, update.CreateTableUpdate):
47+
raise error.SpannerError(
48+
'create_table must be provided a CreateTableUpdate')
4249
all_models = cls.models()
43-
assert schema_change.table() not in all_models
50+
if schema_change.table() in all_models:
51+
raise error.SpannerError(
52+
'Cannot create a table that already exists')
4453
schema_change.validate()
4554

4655
api.SpannerAdminApi.update_schema(schema_change.ddl())
4756

4857
@classmethod
4958
def index_update(cls, schema_change):
50-
assert isinstance(schema_change, update.IndexUpdate)
59+
"""Handles a CREATE INDEX or DROP INDEX schema update."""
60+
if not isinstance(schema_change, update.IndexUpdate):
61+
raise error.SpannerError(
62+
'index_update must be provided a IndexUpdate')
5163
klass = cls.models()[schema_change.table()]
5264
schema_change.validate(klass)
5365

@@ -80,7 +92,7 @@ def make_classmethod(retval):
8092
@classmethod
8193
def _tables(cls, transaction=None):
8294
"""Compiles table information from column schema."""
83-
tables = defaultdict(dict)
95+
tables = collections.defaultdict(dict)
8496
schemas = column.ColumnSchema.where(
8597
transaction, condition.EqualityCondition('table_catalog', ''),
8698
condition.EqualityCondition('table_schema', ''))
@@ -102,15 +114,15 @@ def _indexes(cls, transaction=None):
102114
condition.OrderByCondition(('ordinal_position',
103115
condition.OrderType.ASC)))
104116

105-
index_columns = defaultdict(list)
117+
index_columns = collections.defaultdict(list)
106118
for schema in index_column_schemas:
107119
key = (schema.table_name, schema.index_name)
108120
index_columns[key].append(schema.column_name)
109121

110122
index_schemas = index.IndexSchema.where(
111123
transaction, condition.EqualityCondition('table_catalog', ''),
112124
condition.EqualityCondition('table_schema', ''))
113-
indexes = defaultdict(dict)
125+
indexes = collections.defaultdict(dict)
114126
for schema in index_schemas:
115127
indexes[schema.table_name][schema.index_name] = {
116128
'columns': index_columns[(schema.table_name, schema.index_name)],

spanner_orm/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"""Class that handles API calls to Spanner."""
1616

1717
import abc
18+
19+
from spanner_orm import error
20+
1821
from google.cloud import spanner
1922

2023

@@ -87,7 +90,8 @@ class SpannerApi(SpannerReadApi, SpannerWriteApi):
8790

8891
@classmethod
8992
def _connection(cls):
90-
assert cls._spanner_connection is not None, 'Not connected to Spanner'
93+
if not cls._spanner_connection:
94+
raise error.SpannerError('Not connected to spanner')
9195
return cls._spanner_connection
9296

9397
# Spanner connection methods

spanner_orm/condition.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import abc
1818
import enum
1919

20+
from spanner_orm import error
21+
2022
from google.cloud.spanner_v1.proto import type_pb2
2123

2224

@@ -39,7 +41,9 @@ def bind(self, model):
3941
self.model = model
4042

4143
def params(self):
42-
assert self.model
44+
if not self.model:
45+
raise error.SpannerError(
46+
'Condition must be bound before params is called')
4347
return self._params()
4448

4549
@abc.abstractmethod
@@ -52,15 +56,19 @@ def segment():
5256
raise NotImplementedError
5357

5458
def sql(self):
55-
assert self.model
59+
if not self.model:
60+
raise error.SpannerError(
61+
'Condition must be bound before sql is called')
5662
return self._sql()
5763

5864
@abc.abstractmethod
5965
def _sql(self):
6066
pass
6167

6268
def types(self):
63-
assert self.model
69+
if not self.model:
70+
raise error.SpannerError(
71+
'Condition must be bound before types is called')
6472
return self._types()
6573

6674
@abc.abstractmethod
@@ -120,11 +128,15 @@ def bind(self, model):
120128
self.relation = self.model.relations()[self.name]
121129

122130
def conditions(self):
123-
assert self.relation
131+
if not self.relation:
132+
raise error.SpannerError(
133+
'Condition must be bound before conditions is called')
124134
return self.relation.conditions() + self._conditions
125135

126136
def destination(self):
127-
assert self.relation
137+
if not self.relation:
138+
raise error.SpannerError(
139+
'Condition must be bound before destination is called')
128140
return self.relation.destination()
129141

130142
def relation_name(self):
@@ -156,7 +168,9 @@ class LimitCondition(Condition):
156168

157169
def __init__(self, value):
158170
super().__init__()
159-
assert isinstance(value, int)
171+
if not isinstance(value, int):
172+
raise error.SpannerError(
173+
'{value} is not of type int'.format(value=value))
160174
self.value = value
161175

162176
def _params(self):
@@ -188,7 +202,9 @@ class OrderByCondition(Condition):
188202
def __init__(self, *orderings):
189203
super().__init__()
190204
for (_, order_type) in orderings:
191-
assert isinstance(order_type, OrderType)
205+
if not isinstance(order_type, OrderType):
206+
raise error.SpannerError(
207+
'{order} is not of type OrderType'.format(order=order_type))
192208
self.orderings = orderings
193209

194210
def _params(self):

spanner_orm/error.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# python3
2+
# Copyright 2018 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Generic error to be returned to calling code."""
16+
17+
18+
class SpannerError(Exception):
19+
pass

spanner_orm/model.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from spanner_orm import api
2121
from spanner_orm import condition
22+
from spanner_orm import error
2223
from spanner_orm import query
2324

2425
from google.cloud import spanner
@@ -34,7 +35,6 @@ def column_prefix(cls):
3435
# Table schema class methods
3536
@classmethod
3637
def columns(cls):
37-
assert isinstance(cls.schema(), dict)
3838
return set(cls.schema())
3939

4040
@staticmethod
@@ -73,15 +73,22 @@ def _validate(cls, name, value):
7373
# Instance methods
7474
def __init__(self, values, persisted=False):
7575
# Ensure that we have the primary index keys (unique id) set for all objects
76-
assert not set(self.primary_index_keys()) - set(values.keys())
76+
missing_keys = set(self.primary_index_keys()) - set(values.keys())
77+
if missing_keys:
78+
raise error.SpannerError(
79+
'All primary keys must be specified. Missing: {keys}'.format(
80+
keys=missing_keys))
7781

7882
self.start_values = {
7983
key: copy.copy(value)
8084
for key, value in values.items()
8185
if key in self.columns() and value is not None
8286
}
8387
for name, value in self.start_values.items():
84-
self._validate(name, value)
88+
try:
89+
self._validate(name, value)
90+
except AssertionError as ex:
91+
raise error.SpannerError(*ex.args)
8592
self.values = copy.deepcopy(self.start_values)
8693
self._persisted = persisted
8794

@@ -101,7 +108,10 @@ def __setattr__(self, name, value):
101108
if name in self.primary_index_keys():
102109
raise AttributeError(name)
103110
elif name in self.schema():
104-
self._validate(name, value)
111+
try:
112+
self._validate(name, value)
113+
except AssertionError as ex:
114+
raise error.SpannerError(ex.args)
105115
self.values[name] = copy.copy(value)
106116
else:
107117
super().__setattr__(name, value)
@@ -110,7 +120,10 @@ def __setitem__(self, name, value):
110120
if name in self.primary_index_keys():
111121
raise KeyError(name)
112122
elif name in self.schema():
113-
self._validate(name, value)
123+
try:
124+
self._validate(name, value)
125+
except AssertionError as ex:
126+
raise error.SpannerError(ex.args)
114127
self.values[name] = copy.copy(value)
115128
else:
116129
raise KeyError(name)
@@ -176,7 +189,9 @@ def find(cls, transaction=None, **kwargs):
176189
# Make sure that all primary keys were included (sometimes multiple
177190
# primary keys for a table)
178191
index_keys = list(cls.primary_index_keys())
179-
assert set(kwargs.keys()) == set(index_keys)
192+
if set(kwargs.keys()) != set(index_keys):
193+
raise error.SpannerError(
194+
'All primary index keys must be specified')
180195

181196
# Keys need to be in specfic order
182197
ordered_values = [kwargs[column] for column in index_keys]

0 commit comments

Comments
 (0)