Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

205 lines
5.6 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for node timeout behavior."""
from __future__ import annotations
import asyncio
from google.adk.apps import App
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.workflow import START
from google.adk.workflow._errors import NodeTimeoutError
from google.adk.workflow._node import node
from google.adk.workflow._retry_config import RetryConfig
from google.adk.workflow._workflow import Workflow
from google.genai import types
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _run_workflow(wf, message='start'):
"""Run a Workflow through Runner, return collected events."""
ss = InMemorySessionService()
runner = Runner(app_name='test', node=wf, session_service=ss)
session = await ss.create_session(app_name='test', user_id='u')
msg = types.Content(parts=[types.Part(text=message)], role='user')
events = []
async for event in runner.run_async(
user_id='u', session_id=session.id, new_message=msg
):
events.append(event)
return events, ss, session
def _output_by_node(events):
"""Extract (node_name_from_path, output) for child node events."""
results = []
for e in events:
if e.output is not None and e.node_info.path and '/' in e.node_info.path:
node_name = e.node_info.path.rsplit('/', 1)[-1]
if '@' in node_name:
node_name = node_name.rsplit('@', 1)[0]
results.append((node_name, e.output))
return results
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_node_completes_within_timeout():
"""A node that finishes before the timeout should succeed normally."""
@node(timeout=5.0)
async def my_slow_node():
await asyncio.sleep(0.01)
return 'done'
wf = Workflow(
name='test_workflow',
edges=[
(START, my_slow_node),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ('my_slow_node', 'done') in by_node
@pytest.mark.asyncio
async def test_node_exceeds_timeout():
"""A node that exceeds its timeout should fail."""
from google.adk.workflow import FunctionNode
async def raw_slow_func():
await asyncio.sleep(1.0)
return 'done'
my_too_slow_node = FunctionNode(
name='my_too_slow_node', func=raw_slow_func, timeout=0.05
)
wf = Workflow(
name='test_workflow',
edges=[
(START, my_too_slow_node),
],
)
with pytest.raises(NodeTimeoutError) as exc_info:
await _run_workflow(wf)
assert 'my_too_slow_node' in str(exc_info.value)
@pytest.mark.asyncio
async def test_node_no_timeout():
"""A node with timeout=None should run without any time limit."""
@node(timeout=None)
async def my_no_timeout_node():
await asyncio.sleep(0.01)
return 'done'
wf = Workflow(
name='test_workflow',
edges=[
(START, my_no_timeout_node),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ('my_no_timeout_node', 'done') in by_node
@pytest.mark.asyncio
async def test_node_timeout_with_retry():
"""A timed-out node should be retried if retry_config is set."""
run_count = 0
@node(
timeout=0.05,
retry_config=RetryConfig(max_attempts=3, initial_delay=0.0, jitter=0.0),
)
async def node_a():
nonlocal run_count
run_count += 1
if run_count == 1:
await asyncio.sleep(1.0)
return 'success'
wf = Workflow(
name='test_workflow',
edges=[
(START, node_a),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
# Verify that the final result was successfully obtained.
assert ('node_a', 'success') in by_node
# Verify that the node was actually executed more than once (i.e., retried).
assert run_count == 2, f'Expected run_count == 2, got {run_count}'
@pytest.mark.asyncio
async def test_nested_workflow_timeout():
"""A nested workflow that exceeds its timeout in the outer workflow should fail.
Setup: outer_wf -> inner_wf -> slow_node. inner_wf has timeout=0.05.
Act: Run the outer workflow.
Assert: Execution raises NodeTimeoutError referencing inner_wf.
"""
import sys
if sys.version_info < (3, 11):
pytest.skip('asyncio.timeout requires Python 3.11+')
# Given an outer workflow containing a slow inner workflow with a timeout
@node()
async def slow_node():
await asyncio.sleep(1.0)
return 'done'
inner_wf = Workflow(
name='inner_wf',
edges=[(START, slow_node)],
timeout=0.05,
)
outer_wf = Workflow(
name='outer_wf',
edges=[(START, inner_wf)],
)
# When the outer workflow is executed
# Then it should raise NodeTimeoutError referencing the inner workflow
with pytest.raises(NodeTimeoutError) as exc_info:
await _run_workflow(outer_wf)
assert 'inner_wf' in str(exc_info.value)