Skip to content

Commit 62a8261

Browse files
authored
test(gapic): consolidate async otel tests into test_method_async and share fixtures
1 parent b3efec6 commit 62a8261

4 files changed

Lines changed: 160 additions & 176 deletions

File tree

packages/google-api-core/tests/asyncio/gapic/test_method_async.py

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@
2626
except ImportError:
2727
pytest.skip("No GRPC", allow_module_level=True)
2828

29+
from google.api_core import (
30+
client_options as client_options_lib,
31+
)
2932
from google.api_core import (
3033
exceptions,
3134
gapic_v1,
3235
grpc_helpers_async,
3336
retry_async,
3437
timeout,
3538
)
39+
from google.api_core.gapic_v1 import client_info
40+
from tests.helpers import assert_uninstrumented_gapic_callable
3641

3742

3843
def _utcnow_monotonic():
@@ -276,50 +281,104 @@ async def test_wrap_method_without_wrap_errors():
276281
method.assert_not_called()
277282

278283

284+
_ASYNC_SERVICE_DEFAULT_SPAN_ATTRIBUTES = {
285+
"rpc.system": "grpc",
286+
"rpc.service": "google.test.AsyncService",
287+
"rpc.method": "AsyncMethod",
288+
"gcp.client.service": "AsyncService",
289+
"gcp.client.repo": "googleapis/google-cloud-python",
290+
}
291+
292+
279293
@pytest.mark.asyncio
280-
async def test_wrap_method_async_with_otel_tracing(monkeypatch):
281-
import sys
294+
async def test_wrap_method_async_with_otel_tracing(mock_otel):
295+
"""Proves that method_async.wrap_method creates a T3 span upon invocation."""
296+
fake_call = grpc_helpers_async.FakeUnaryUnaryCall(42)
297+
method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call)
298+
299+
wrapped_method = gapic_v1.method_async.wrap_method(
300+
method,
301+
method_name="google.test.AsyncService/AsyncMethod",
302+
)
303+
result = await wrapped_method(1, 2, meep="moop")
282304

283-
monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true")
305+
assert result == 42
306+
mock_otel.tracer.start_as_current_span.assert_called_once_with(
307+
"google.test.AsyncService/AsyncMethod",
308+
kind="CLIENT",
309+
attributes=_ASYNC_SERVICE_DEFAULT_SPAN_ATTRIBUTES,
310+
)
311+
312+
313+
@pytest.mark.asyncio
314+
async def test_wrap_method_async_otel_tracing_streaming_skips_span(mock_otel):
315+
"""Proves that method_async.wrap_method with is_streaming=True skips span creation."""
284316
fake_call = grpc_helpers_async.FakeUnaryUnaryCall(42)
285317
method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call)
286318

287-
mock_span = mock.MagicMock()
288-
mock_tracer = mock.MagicMock()
289-
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
319+
wrapped = gapic_v1.method_async.wrap_method(
320+
method,
321+
method_name="google.test.AsyncService/AsyncMethod",
322+
is_streaming=True,
323+
)
324+
result = await wrapped(1, 2)
325+
326+
assert_uninstrumented_gapic_callable(
327+
wrapped,
328+
result,
329+
method,
330+
mock_trace=mock_otel.trace,
331+
expected_result=42,
332+
)
290333

291-
mock_trace = mock.Mock()
292-
mock_trace.get_tracer.return_value = mock_tracer
293-
mock_trace.SpanKind.CLIENT = "CLIENT"
294334

295-
with (
296-
mock.patch(
297-
"google.api_core._observability.is_otel_capabilities_enabled",
298-
return_value=True,
299-
),
300-
mock.patch.dict(
301-
sys.modules,
302-
{
303-
"opentelemetry": mock.Mock(trace=mock_trace),
304-
"opentelemetry.trace": mock_trace,
305-
},
306-
),
307-
):
308-
wrapped_method = gapic_v1.method_async.wrap_method(
309-
method,
310-
method_name="google.test.AsyncService/AsyncMethod",
311-
)
312-
result = await wrapped_method(1, 2, meep="moop")
335+
@pytest.mark.asyncio
336+
async def test_wrap_method_async_otel_tracing_custom_client_options(mock_otel):
337+
"""Proves that method_async.wrap_method forwards custom client_options tracer_provider."""
338+
fake_call = grpc_helpers_async.FakeUnaryUnaryCall(42)
339+
method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call)
340+
341+
mock_provider = mock.Mock()
342+
mock_provider.get_tracer.return_value = mock_otel.tracer
343+
client_options = client_options_lib.ClientOptions(tracer_provider=mock_provider)
344+
345+
wrapped = gapic_v1.method_async.wrap_method(
346+
method,
347+
client_options=client_options,
348+
method_name="google.test.AsyncService/AsyncMethod",
349+
)
350+
result = await wrapped(1, 2)
351+
352+
assert result == 42
353+
mock_provider.get_tracer.assert_called_once_with("google.api_core")
354+
mock_otel.tracer.start_as_current_span.assert_called_once_with(
355+
"google.test.AsyncService/AsyncMethod",
356+
kind="CLIENT",
357+
attributes=_ASYNC_SERVICE_DEFAULT_SPAN_ATTRIBUTES,
358+
)
359+
360+
361+
@pytest.mark.asyncio
362+
async def test_wrap_method_async_otel_tracing_with_client_info(mock_otel):
363+
"""Proves that method_async.wrap_method passes client_info to _GapicCallable."""
364+
fake_call = grpc_helpers_async.FakeUnaryUnaryCall(42)
365+
method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call)
366+
367+
info = client_info.ClientInfo(client_library_version="3.0.0")
368+
369+
wrapped = gapic_v1.method_async.wrap_method(
370+
method,
371+
client_info=info,
372+
method_name="google.test.AsyncService/AsyncMethod",
373+
)
374+
result = await wrapped(1, 2)
313375

314376
assert result == 42
315-
mock_tracer.start_as_current_span.assert_called_once_with(
377+
mock_otel.tracer.start_as_current_span.assert_called_once_with(
316378
"google.test.AsyncService/AsyncMethod",
317379
kind="CLIENT",
318380
attributes={
319-
"rpc.system": "grpc",
320-
"rpc.service": "google.test.AsyncService",
321-
"rpc.method": "AsyncMethod",
322-
"gcp.client.service": "AsyncService",
323-
"gcp.client.repo": "googleapis/google-cloud-python",
381+
**_ASYNC_SERVICE_DEFAULT_SPAN_ATTRIBUTES,
382+
"gcp.client.version": "3.0.0",
324383
},
325384
)

packages/google-api-core/tests/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515
import os
16+
import sys
17+
import types
1618
from unittest import mock
1719

1820
import pytest
@@ -29,3 +31,36 @@ def mock_mtls_env():
2931
},
3032
):
3133
yield
34+
35+
36+
@pytest.fixture
37+
def mock_otel(monkeypatch):
38+
"""Provides a mocked OpenTelemetry environment with tracing enabled."""
39+
monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true")
40+
mock_span = mock.MagicMock()
41+
mock_tracer = mock.MagicMock()
42+
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
43+
44+
mock_trace = mock.Mock()
45+
mock_trace.get_tracer.return_value = mock_tracer
46+
mock_trace.SpanKind.CLIENT = "CLIENT"
47+
mock_trace.StatusCode.ERROR = "ERROR"
48+
49+
with (
50+
mock.patch(
51+
"google.api_core._observability.is_otel_capabilities_enabled",
52+
return_value=True,
53+
),
54+
mock.patch.dict(
55+
sys.modules,
56+
{
57+
"opentelemetry": mock.Mock(trace=mock_trace),
58+
"opentelemetry.trace": mock_trace,
59+
},
60+
),
61+
):
62+
yield types.SimpleNamespace(
63+
trace=mock_trace,
64+
tracer=mock_tracer,
65+
span=mock_span,
66+
)

packages/google-api-core/tests/helpers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,30 @@ def parse_responses(response_message_cls, all_responses: List[proto.Message]) ->
7878
DeprecationWarning,
7979
match="argument is deprecated because of a potential security risk",
8080
)
81+
82+
83+
def assert_uninstrumented_gapic_callable(
84+
wrapped,
85+
result,
86+
mock_target,
87+
mock_trace=None,
88+
expected_result="success",
89+
):
90+
"""Verifies that an uninstrumented RPC callable succeeds without tracing overhead.
91+
92+
1. Proves the RPC executed successfully with the expected return value.
93+
2. Proves the OpenTelemetry API was never invoked.
94+
3. Proves the callable holds no tracer or span configuration.
95+
"""
96+
# 1. Prove the RPC executed successfully
97+
assert result == expected_result
98+
mock_target.assert_called_once()
99+
100+
# 2. Prove the OpenTelemetry API was never invoked
101+
if mock_trace is not None:
102+
mock_trace.get_tracer.assert_not_called()
103+
104+
# 3. Prove the callable holds no tracer or span configuration
105+
assert wrapped._tracer is None
106+
assert wrapped._span_name is None
107+
assert wrapped._span_attributes is None

0 commit comments

Comments
 (0)