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
1518from 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