Skip to content

Commit bc6b546

Browse files
RKestcopybara-github
authored andcommitted
test: add functional telemetry tests
This includes: - Test verifying multiple spans are written during E2E runner execution. - Regression tests for the "ContextVar was created in a different Context" exceptions caused by the interplay of context based instrumentation and async generators getting indeterminately suspended. PiperOrigin-RevId: 804333483
1 parent 1e23652 commit bc6b546

File tree

4 files changed

+157
-7
lines changed

4 files changed

+157
-7
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import gc
16+
import sys
17+
from unittest import mock
18+
19+
from google.adk import telemetry
20+
from google.adk.agents import base_agent
21+
from google.adk.agents.llm_agent import Agent
22+
from google.adk.models.base_llm import BaseLlm
23+
from google.adk.tools import FunctionTool
24+
from google.adk.utils.context_utils import Aclosing
25+
from google.genai.types import Part
26+
from opentelemetry.version import __version__
27+
import pytest
28+
29+
from ..testing_utils import MockModel
30+
from ..testing_utils import TestInMemoryRunner
31+
32+
33+
@pytest.fixture
34+
def test_model() -> BaseLlm:
35+
mock_model = MockModel.create(
36+
responses=[
37+
Part.from_function_call(name='some_tool', args={}),
38+
Part.from_text(text='text response'),
39+
]
40+
)
41+
return mock_model
42+
43+
44+
@pytest.fixture
45+
def test_agent(test_model: BaseLlm) -> Agent:
46+
def some_tool():
47+
pass
48+
49+
root_agent = Agent(
50+
name='some_root_agent',
51+
model=test_model,
52+
tools=[
53+
FunctionTool(some_tool),
54+
],
55+
)
56+
return root_agent
57+
58+
59+
@pytest.fixture
60+
async def test_runner(test_agent: Agent) -> TestInMemoryRunner:
61+
runner = TestInMemoryRunner(test_agent)
62+
return runner
63+
64+
65+
@pytest.fixture
66+
def mock_start_as_current_span(monkeypatch: pytest.MonkeyPatch) -> mock.Mock:
67+
mock_context_manager = mock.MagicMock()
68+
mock_context_manager.__enter__.return_value = mock.Mock()
69+
mock_start_as_current_span = mock.Mock()
70+
mock_start_as_current_span.return_value = mock_context_manager
71+
72+
def do_replace(tracer):
73+
monkeypatch.setattr(
74+
tracer, 'start_as_current_span', mock_start_as_current_span
75+
)
76+
77+
do_replace(telemetry.tracer)
78+
do_replace(base_agent.tracer)
79+
80+
return mock_start_as_current_span
81+
82+
83+
@pytest.mark.asyncio
84+
async def test_tracer_start_as_current_span(
85+
test_runner: TestInMemoryRunner,
86+
mock_start_as_current_span: mock.Mock,
87+
):
88+
"""Test creation of multiple spans in an E2E runner invocation.
89+
90+
Additionally tests if each async generator invoked is wrapped in Aclosing.
91+
This is necessary because instrumentation utilizes contextvars, which ran into "ContextVar was created in a different Context" errors,
92+
when a given coroutine gets indeterminitely suspended.
93+
"""
94+
firstiter, finalizer = sys.get_asyncgen_hooks()
95+
96+
def wrapped_firstiter(coro):
97+
nonlocal firstiter
98+
assert any(
99+
isinstance(referrer, Aclosing)
100+
or isinstance(indirect_referrer, Aclosing)
101+
for referrer in gc.get_referrers(coro)
102+
# Some coroutines have a layer of indirection in python 3.9 and 3.10
103+
for indirect_referrer in gc.get_referrers(referrer)
104+
), f'Coro `{coro.__name__}` is not wrapped with Aclosing'
105+
firstiter(coro)
106+
107+
sys.set_asyncgen_hooks(wrapped_firstiter, finalizer)
108+
109+
# Act
110+
async with Aclosing(test_runner.run_async_with_new_session_agen('')) as agen:
111+
async for _ in agen:
112+
pass
113+
114+
# Assert
115+
expected_start_as_current_span_calls = [
116+
mock.call('invocation'),
117+
mock.call('execute_tool some_tool'),
118+
mock.call('agent_run [some_root_agent]'),
119+
mock.call('call_llm'),
120+
mock.call('call_llm'),
121+
]
122+
123+
mock_start_as_current_span.assert_has_calls(
124+
expected_start_as_current_span_calls,
125+
any_order=True,
126+
)
127+
assert mock_start_as_current_span.call_count == len(
128+
expected_start_as_current_span_calls
129+
)
File renamed without changes.

tests/unittests/testing_utils.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from google.adk.runners import Runner
3737
from google.adk.sessions.in_memory_session_service import InMemorySessionService
3838
from google.adk.sessions.session import Session
39+
from google.adk.utils.context_utils import Aclosing
3940
from google.genai import types
4041
from google.genai.types import Part
4142
from typing_extensions import override
@@ -161,19 +162,26 @@ async def run_async_with_new_session(
161162
self, new_message: types.ContentUnion
162163
) -> list[Event]:
163164

165+
collected_events: list[Event] = []
166+
async for event in self.run_async_with_new_session_agen(new_message):
167+
collected_events.append(event)
168+
169+
return collected_events
170+
171+
async def run_async_with_new_session_agen(
172+
self, new_message: types.ContentUnion
173+
) -> AsyncGenerator[Event, None]:
164174
session = await self.session_service.create_session(
165175
app_name='InMemoryRunner', user_id='test_user'
166176
)
167-
collected_events = []
168-
169-
async for event in self.run_async(
177+
agen = self.run_async(
170178
user_id=session.user_id,
171179
session_id=session.id,
172180
new_message=get_user_content(new_message),
173-
):
174-
collected_events.append(event)
175-
176-
return collected_events
181+
)
182+
async with Aclosing(agen):
183+
async for event in agen:
184+
yield event
177185

178186

179187
class InMemoryRunner:

0 commit comments

Comments
 (0)