Files
google--adk-python/.agents/skills/adk-verify-snippets/scripts/run.py
T
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

357 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
from __future__ import annotations
import argparse
import asyncio
import importlib.util
import os
from pathlib import Path
import sys
import traceback
# Sentinel string used by verify_md.py to locate and split the coverage section
# out of run.py's stdout. Keep in sync with verify_md.py:COV_SECTION_HEADER.
COV_SECTION_HEADER = "📊 Phase 4: Code Coverage Report"
# Structured exit codes — consumed by verify_md.py to classify results without
# fragile string/emoji matching. Keep in sync with verify_md.py:EXIT_* constants.
EXIT_SUCCESS = 0 # All phases passed
EXIT_LOAD_FAILURE = 1 # Failed to compile / load the snippet
EXIT_RUN_FAILURE = 2 # Loaded OK but the ADK component failed at runtime
EXIT_NO_COMPONENT = 3 # Loaded OK, no runnable ADK component found (load-only)
# --- Optional Coverage Integration ---
try:
import coverage
HAS_COVERAGE = True
except ImportError:
HAS_COVERAGE = False
# --- Imports for ADK Inspection ---
from google.adk.agents.base_agent import BaseAgent
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 Workflow
from google.genai import types
def load_target_module(file_path: Path):
"""Dynamically loads a Python file as a module, catching import/compilation/definition errors."""
# Use the absolute path string as the key to avoid collisions when multiple
# snippets share the same file stem or when the stem matches an installed package.
module_name = str(file_path)
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None:
raise ImportError(
f"Could not resolve module spec for file '{file_path.name}'"
)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
# Executing the module runs all top-level code, which will catch:
# - SyntaxError / IndentationError
# - ImportError (e.g. from google.adk.workflow import build_node)
# - ValidationError (e.g. instantiating Workflow with invalid edges)
try:
spec.loader.exec_module(module)
except Exception:
# Remove the partially-initialised module so a broken entry is never
# left in sys.modules for the lifetime of this process. This matters
# when run.py is imported in-process (e.g. from a test harness) rather
# than invoked as a subprocess.
sys.modules.pop(module_name, None)
raise
return module
def discover_adk_component(module):
"""Scans the module namespace to discover runnable ADK components, prioritizing root components.
Uses two passes to correctly identify root agents regardless of the order
in which names appear in ``vars(module)``:
* Pass 1 — collect every Workflow, Agent, and App in the module namespace.
* Pass 2 — build the full set of sub-agent IDs from *all* collected agents,
then filter to find agents that are not sub-agents of any other agent.
Without the two-pass approach, a root agent whose variable name is seen
before its sub-agents (e.g. ``root`` defined above ``child`` in the file)
would be encountered first, before ``child``'s own sub-agents are registered,
causing incorrect root detection.
"""
workflows = []
agents = []
apps = []
# Pass 1: collect all candidate components.
#
# Use vars(module) rather than inspect.getmembers(module) because
# getmembers() invokes every attribute getter and silently swallows any
# Exception raised by broken descriptors or properties — a snippet that
# defines an Agent behind a faulty @property would simply be missing from
# the scan with no error or log entry. vars(module) reads the module's
# __dict__ directly, which never triggers descriptors and never suppresses
# exceptions, giving us an accurate view of module-level names.
for obj in vars(module).values():
if isinstance(obj, Workflow):
workflows.append(obj)
elif isinstance(obj, BaseAgent):
agents.append(obj)
elif isinstance(obj, App):
apps.append(obj)
# 1. Prefer Workflow
if workflows:
return workflows[0], "Workflow"
# Pass 2: build the complete sub-agent ID set now that all agents are known,
# then select the root (any agent not listed as a sub-agent of another).
#
# Read sub_agents into a local snapshot rather than calling the attribute
# twice. Calling it twice is unsafe when sub_agents is a non-idempotent
# property: the first call (guard) and the second call (iteration) could
# return different objects, causing id() values to diverge and root
# detection to silently misfire.
sub_agent_ids: set[int] = set()
for agent in agents:
children = getattr(agent, "sub_agents", None) or []
for sub in children:
sub_agent_ids.add(id(sub))
# 2. Find root Agent (not a sub-agent of any other agent in the module)
root_agents = [a for a in agents if id(a) not in sub_agent_ids]
if root_agents:
return root_agents[0], "Agent"
# 3. Fall back to App
if apps:
return apps[0], "App"
return None, None
async def run_component(component, component_type, test_input):
"""Unified runner to execute the discovered component."""
print(f"\n🔍 Discovered ADK {component_type} in target file.")
print(f"🚀 Running execution test with input: '{test_input}'...\n")
if component_type == "App":
runnable_node = getattr(component, "root_agent", None)
if runnable_node is None:
raise AttributeError(
f"App instance has no 'root_agent' attribute. "
f"Ensure the App is constructed with a root_agent argument."
)
else:
runnable_node = component
session_service = InMemorySessionService()
runner = Runner(
app_name="runnability_test",
node=runnable_node,
session_service=session_service,
)
session = await session_service.create_session(
app_name="runnability_test", user_id="tester"
)
user_message = types.Content(
parts=[types.Part(text=str(test_input))], role="user"
)
async for event in runner.run_async(
user_id="tester", session_id=session.id, new_message=user_message
):
print(f"🎬 [Event] Author: {event.author}")
if event.output:
print(f"🔹 Output: {event.output}")
if hasattr(event, "content") and event.content and event.content.parts:
text = "".join(p.text for p in event.content.parts if p.text)
if text:
print(f"📝 Content Output:\n{'-'*40}\n{text}\n{'-'*40}")
def main():
parser = argparse.ArgumentParser(
description="Generalized ADK Runnability & Loadability Tester"
)
parser.add_argument(
"file",
type=str,
help="Path to the python file containing the agent/workflow to test",
)
args = parser.parse_args()
file_path = Path(args.file).resolve()
if not file_path.exists():
print(f"❌ Error: File '{file_path}' does not exist.")
sys.exit(EXIT_LOAD_FAILURE)
print(f"🔬 Testing file: {file_path.name}")
print("=" * 60)
# Initialize coverage programmatically to track ONLY the target file.
#
# Implementation note: snippets are loaded via importlib/exec_module, which
# CPython's sys.settrace-based tracer instruments correctly *only* if the
# tracer is active before the module's code object is compiled and executed.
# Starting coverage here — before load_target_module() — satisfies that
# requirement. The `include` filter ensures no ADK library code is counted.
cov = None
if HAS_COVERAGE:
cov = coverage.Coverage(
branch=True,
data_file=None, # Keep coverage data in-memory only, no .coverage file needed
include=[str(file_path)], # Scope collection to the snippet file only
)
cov.start()
else:
print(
"️ Install 'coverage' package to enable automated code coverage"
" reporting."
)
# exit_code is set by each phase and consumed inside the finally block so
# that coverage reporting always runs before the process exits. Using a
# mutable list as a simple cell lets the finally clause read the value set
# by any code path (normal completion, early break-out via a flag, or an
# unexpected exception) without requiring nonlocal or a class wrapper.
exit_code = [EXIT_SUCCESS]
try:
# 1. Test Loadability (Imports, Syntax, Instantiation/Validation)
print("📋 Phase 1: Loading & Compiling...")
try:
module = load_target_module(file_path)
print(
f"✅ Load Success: '{file_path.name}' compiled and loaded without any"
" issues."
)
except Exception:
print(f"❌ Load Failure: Failed to compile/load '{file_path.name}':")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_LOAD_FAILURE
# Do NOT return here. Fall through to the finally block so that
# coverage is reported and sys.exit() is called with the correct code.
# The module variable is not set, so we skip phases 23 via the flag.
else:
# 2. Discover Component (only reached when load succeeded)
print("\n📋 Phase 2: Component Discovery...")
component, comp_type = discover_adk_component(module)
if not component:
print(
" NO ADK COMPONENT: No module-level Workflow, Agent, or App"
f" instance found in '{file_path.name}'."
)
print(
" Runnability test skipped. To enable it, assign a Workflow,"
" Agent, or App"
)
print(
" to a module-level variable (e.g. `agent = Agent(...)`). The"
" variable name"
)
print(
" does not matter — the runner detects it automatically via"
" vars(module)."
)
print(
"️ Coverage below reflects load-time execution only (module-level"
" statements)."
)
exit_code[0] = EXIT_NO_COMPONENT
else:
# Get test input from module, or fallback
test_input = getattr(module, "test_input", "Test input topic")
# 3. Test Runnability
print(f"\n📋 Phase 3: Executing {comp_type}...")
try:
# asyncio.run() creates a fresh event loop each time, so it will raise
# RuntimeError if a loop is already running (e.g. the snippet called
# asyncio.run() at module level without an __main__ guard).
# We catch that specific case and report it clearly, rather than using
# the deprecated asyncio.get_event_loop() API (removed in Python 3.12+).
asyncio.run(run_component(component, comp_type, test_input))
print(
f"\n✅ Run Success: Component '{comp_type}' executed"
" successfully."
)
except RuntimeError as e:
if "event loop" in str(e).lower():
print(
f"\n❌ Run Failure: An event loop conflict was detected after"
f" module load."
)
print(
" The snippet likely called asyncio.run() at module level,"
" which"
)
print(
" conflicts with the runner's own event loop. Wrap top-level"
" async"
)
print(
" calls in an `if __name__ == '__main__':` guard, or"
" annotate the"
)
print(" snippet with <!-- verify-snippets: ignore -->.")
else:
print(f"\n❌ Run Failure: Component failed during execution:")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_RUN_FAILURE
except Exception:
print(f"\n❌ Run Failure: Component failed during execution:")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_RUN_FAILURE
finally:
# Coverage reporting runs here so it is guaranteed to execute on every
# code path: normal completion, load failure, no-component, run failure.
if cov:
cov.stop()
print(f"\n{COV_SECTION_HEADER} (Target File)")
print("=" * 60)
try:
# Report coverage of the target file directly to stdout
cov.report(morfs=[str(file_path)], file=sys.stdout)
except coverage.exceptions.NoDataError:
print(
"⚠️ No coverage data collected (compilation or execution failed"
" early)."
)
except Exception as ce:
print(f"⚠️ Failed to generate coverage report: {ce}")
print("=" * 60)
# Only call sys.exit for non-zero codes. If exit_code is EXIT_SUCCESS
# we return normally so that any exception currently propagating out of
# the try block is not silently replaced by a SystemExit raised here.
if exit_code[0] != EXIT_SUCCESS:
sys.exit(exit_code[0])
if __name__ == "__main__":
main()