Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit a34c01e

Browse files
committed
first round of tests
1 parent f07e765 commit a34c01e

2 files changed

Lines changed: 283 additions & 17 deletions

File tree

google/cloud/bigtable/data/_async/metrics_interceptor.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,17 @@ async def intercept_unary_unary(
114114
raise
115115
finally:
116116
if call is not None:
117-
metadata = (
118-
await call.trailing_metadata() + await call.initial_metadata()
119-
)
117+
metadata = (await call.trailing_metadata() or []) + (await call.initial_metadata() or [])
120118
operation.add_response_metadata(metadata)
121-
if encountered_exc is not None:
122-
# end attempt. If it succeeded, let higher levels decide when to end operation
123-
operation.end_attempt_with_status(encountered_exc)
119+
if encountered_exc is not None:
120+
# end attempt. If it succeeded, let higher levels decide when to end operation
121+
operation.end_attempt_with_status(encountered_exc)
124122

125123
@CrossSync.convert
126124
@_with_operation_from_metadata
127125
async def intercept_unary_stream(
128126
self, operation, continuation, client_call_details, request
129127
):
130-
# TODO: benchmark
131128
async def response_wrapper(call):
132129
has_first_response = operation.first_response_latency is not None
133130
encountered_exc = None
@@ -145,12 +142,11 @@ async def response_wrapper(call):
145142
encountered_exc = e
146143
raise
147144
finally:
148-
metadata = (
149-
await call.trailing_metadata() + await call.initial_metadata()
150-
)
151-
operation.add_response_metadata(metadata)
152-
if encountered_exc is not None:
153-
# end attempt. If it succeeded, let higher levels decide when to end operation
154-
operation.end_attempt_with_status(encountered_exc)
145+
if call is not None:
146+
metadata = (await call.trailing_metadata() or []) + (await call.initial_metadata() or [])
147+
operation.add_response_metadata(metadata)
148+
if encountered_exc is not None:
149+
# end attempt. If it succeeded, let higher levels decide when to end operation
150+
operation.end_attempt_with_status(encountered_exc)
155151

156152
return response_wrapper(await continuation(client_call_details, request))

tests/unit/data/_async/test_metrics_interceptor.py

Lines changed: 273 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,282 @@
1010
# distributed under the License is distributed on an "AS IS" BASIS,
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
13-
# limitations under the License
13+
# limitations under the License.
14+
15+
import pytest
16+
import asyncio
1417

1518
from google.cloud.bigtable.data._cross_sync import CrossSync
1619

20+
# try/except added for compatibility with python < 3.8
21+
try:
22+
from unittest import mock
23+
except ImportError: # pragma: NO COVER
24+
import mock # type: ignore
25+
1726

1827
__CROSS_SYNC_OUTPUT__ = "tests.unit.data._sync_autogen.test_metrics_interceptor"
1928

20-
@CrossSync.convert_class
21-
class TestMetricsInterceptor:
29+
30+
class _AsyncIterator:
31+
"""Helper class to wrap an iterator or async generator in an async iterator"""
32+
33+
def __init__(self, iterable):
34+
if hasattr(iterable, "__anext__"):
35+
self._iterator = iterable
36+
else:
37+
self._iterator = iter(iterable)
38+
39+
def __aiter__(self):
40+
return self
41+
42+
async def __anext__(self):
43+
if hasattr(self._iterator, "__anext__"):
44+
return await self._iterator.__anext__()
45+
try:
46+
return next(self._iterator)
47+
except StopIteration:
48+
raise StopAsyncIteration
49+
50+
51+
@CrossSync.convert_class(sync_name="TestMetricsInterceptor")
52+
class TestMetricsInterceptorAsync:
53+
@staticmethod
54+
@CrossSync.convert
55+
def _get_target_class():
56+
from google.cloud.bigtable.data._async import metrics_interceptor
57+
58+
return metrics_interceptor.AsyncBigtableMetricsInterceptor
59+
60+
def _make_one(self, *args, **kwargs):
61+
return self._get_target_class()(*args, **kwargs)
62+
63+
def test_ctor(self):
64+
instance = self._make_one()
65+
assert instance.operation_map == {}
66+
67+
def test_register_operation(self):
68+
"""
69+
adding a new operation should register it in operation_map
70+
"""
71+
from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric
72+
from google.cloud.bigtable.data._metrics.data_model import OperationType
73+
74+
instance = self._make_one()
75+
op = ActiveOperationMetric(OperationType.READ_ROWS)
76+
instance.register_operation(op)
77+
assert instance.operation_map[op.uuid] == op
78+
assert instance in op.handlers
79+
80+
def test_on_operation_comple_mock(self):
81+
"""
82+
completing or cancelling an operation should call on_operation_complete on interceptor
83+
"""
84+
from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric
85+
from google.cloud.bigtable.data._metrics.data_model import OperationType
86+
87+
instance = self._make_one()
88+
instance.on_operation_complete = mock.Mock()
89+
op = ActiveOperationMetric(OperationType.READ_ROWS)
90+
instance.register_operation(op)
91+
op.end_with_success()
92+
assert instance.on_operation_complete.call_count == 1
93+
op.cancel()
94+
assert instance.on_operation_complete.call_count == 2
95+
96+
def test_on_operation_complete(self):
97+
"""
98+
completing an operation should remove it from the operation map
99+
"""
100+
from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric
101+
from google.cloud.bigtable.data._metrics.data_model import OperationType
102+
103+
instance = self._make_one()
104+
op = ActiveOperationMetric(OperationType.READ_ROWS)
105+
instance.register_operation(op)
106+
op.end_with_success()
107+
instance.on_operation_complete(op)
108+
assert op.uuid not in instance.operation_map
109+
110+
def test_on_operation_cancelled(self):
111+
"""
112+
completing an operation should remove it from the operation map
113+
"""
114+
from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric
115+
from google.cloud.bigtable.data._metrics.data_model import OperationType
116+
117+
instance = self._make_one()
118+
op = ActiveOperationMetric(OperationType.READ_ROWS)
119+
instance.register_operation(op)
120+
op.cancel()
121+
assert op.uuid not in instance.operation_map
122+
123+
@CrossSync.pytest
124+
async def test_unary_unary_interceptor_op_not_found(self):
125+
"""Test that interceptor call cuntinuation if op is not found"""
126+
instance = self._make_one()
127+
continuation = CrossSync.Mock()
128+
details = mock.Mock()
129+
details.metadata = []
130+
request = mock.Mock()
131+
await instance.intercept_unary_unary(continuation, details, request)
132+
continuation.assert_called_once_with(details, request)
133+
134+
@CrossSync.pytest
135+
async def test_unary_unary_interceptor_success(self):
136+
"""Test that interceptor handles successful unary-unary calls"""
137+
from google.cloud.bigtable.data._metrics.data_model import (
138+
OPERATION_INTERCEPTOR_METADATA_KEY,
139+
)
140+
141+
instance = self._make_one()
142+
op = mock.Mock()
143+
op.uuid = "test-uuid"
144+
op.state = 1 # ACTIVE_ATTEMPT
145+
instance.operation_map[op.uuid] = op
146+
continuation = CrossSync.Mock()
147+
call = continuation.return_value
148+
call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
149+
call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
150+
details = mock.Mock()
151+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
152+
request = mock.Mock()
153+
result = await instance.intercept_unary_unary(continuation, details, request)
154+
assert result == call
155+
continuation.assert_called_once_with(details, request)
156+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
157+
op.end_attempt_with_status.assert_not_called()
158+
159+
@CrossSync.pytest
160+
async def test_unary_unary_interceptor_failure(self):
161+
"""Test that interceptor handles failed unary-unary calls"""
162+
from google.cloud.bigtable.data._metrics.data_model import (
163+
OPERATION_INTERCEPTOR_METADATA_KEY,
164+
)
165+
166+
instance = self._make_one()
167+
op = mock.Mock()
168+
op.uuid = "test-uuid"
169+
op.state = 1 # ACTIVE_ATTEMPT
170+
instance.operation_map[op.uuid] = op
171+
exc = ValueError("test")
172+
continuation = CrossSync.Mock(side_effect=exc)
173+
call = continuation.return_value
174+
call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
175+
call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
176+
details = mock.Mock()
177+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
178+
request = mock.Mock()
179+
with pytest.raises(ValueError) as e:
180+
await instance.intercept_unary_unary(continuation, details, request)
181+
assert e.value == exc
182+
continuation.assert_called_once_with(details, request)
183+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
184+
op.end_attempt_with_status.assert_called_once_with(exc)
185+
186+
@CrossSync.pytest
187+
async def test_unary_stream_interceptor_op_not_found(self):
188+
"""Test that interceptor calls continuation if op is not found"""
189+
instance = self._make_one()
190+
continuation = CrossSync.Mock()
191+
details = mock.Mock()
192+
details.metadata = []
193+
request = mock.Mock()
194+
await instance.intercept_unary_stream(continuation, details, request)
195+
continuation.assert_called_once_with(details, request)
196+
197+
@CrossSync.pytest
198+
async def test_unary_stream_interceptor_success(self):
199+
"""Test that interceptor handles successful unary-stream calls"""
200+
from google.cloud.bigtable.data._metrics.data_model import (
201+
OPERATION_INTERCEPTOR_METADATA_KEY,
202+
)
203+
204+
instance = self._make_one()
205+
op = mock.Mock()
206+
op.uuid = "test-uuid"
207+
op.state = 1 # ACTIVE_ATTEMPT
208+
op.start_time_ns = 0
209+
op.first_response_latency = None
210+
instance.operation_map[op.uuid] = op
211+
212+
continuation = CrossSync.Mock()
213+
call = continuation.return_value
214+
call.__aiter__ = mock.Mock(return_value=_AsyncIterator([1, 2]))
215+
call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
216+
call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
217+
details = mock.Mock()
218+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
219+
request = mock.Mock()
220+
wrapper = await instance.intercept_unary_stream(continuation, details, request)
221+
results = [val async for val in wrapper]
222+
assert results == [1, 2]
223+
continuation.assert_called_once_with(details, request)
224+
assert op.first_response_latency_ns is not None
225+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
226+
op.end_attempt_with_status.assert_not_called()
227+
228+
@CrossSync.pytest
229+
async def test_unary_stream_interceptor_failure_mid_stream(self):
230+
"""Test that interceptor handles failures mid-stream"""
231+
from google.cloud.bigtable.data._metrics.data_model import (
232+
OPERATION_INTERCEPTOR_METADATA_KEY,
233+
)
234+
235+
instance = self._make_one()
236+
op = mock.Mock()
237+
op.uuid = "test-uuid"
238+
op.state = 1 # ACTIVE_ATTEMPT
239+
op.start_time_ns = 0
240+
op.first_response_latency = None
241+
instance.operation_map[op.uuid] = op
242+
exc = ValueError("test")
243+
244+
continuation = CrossSync.Mock()
245+
call = continuation.return_value
246+
async def mock_generator():
247+
yield 1
248+
raise exc
249+
call.__aiter__ = mock.Mock(return_value=_AsyncIterator(mock_generator()))
250+
call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")])
251+
call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")])
252+
details = mock.Mock()
253+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
254+
request = mock.Mock()
255+
wrapper = await instance.intercept_unary_stream(continuation, details, request)
256+
with pytest.raises(ValueError) as e:
257+
[val async for val in wrapper]
258+
assert e.value == exc
259+
continuation.assert_called_once_with(details, request)
260+
assert op.first_response_latency_ns is not None
261+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
262+
op.end_attempt_with_status.assert_called_once_with(exc)
263+
264+
@CrossSync.pytest
265+
async def test_unary_stream_interceptor_failure_start_stream(self):
266+
"""Test that interceptor handles failures at start of stream"""
267+
from google.cloud.bigtable.data._metrics.data_model import (
268+
OPERATION_INTERCEPTOR_METADATA_KEY,
269+
)
270+
271+
instance = self._make_one()
272+
op = mock.Mock()
273+
op.uuid = "test-uuid"
274+
op.state = 1 # ACTIVE_ATTEMPT
275+
op.start_time_ns = 0
276+
op.first_response_latency = None
277+
instance.operation_map[op.uuid] = op
278+
exc = ValueError("test")
279+
280+
continuation = CrossSync.Mock()
281+
continuation.side_effect = exc
282+
details = mock.Mock()
283+
details.metadata = [(OPERATION_INTERCEPTOR_METADATA_KEY, op.uuid)]
284+
request = mock.Mock()
285+
with pytest.raises(ValueError) as e:
286+
await instance.intercept_unary_stream(continuation, details, request)
287+
assert e.value == exc
288+
continuation.assert_called_once_with(details, request)
289+
assert op.first_response_latency_ns is not None
290+
op.add_response_metadata.assert_called_once_with([("a", "b"), ("c", "d")])
291+
op.end_attempt_with_status.assert_called_once_with(exc)

0 commit comments

Comments
 (0)