Skip to content

Commit fe7ca86

Browse files
committed
feat(adk): Implement e2e testing and CI
1 parent 61c1e52 commit fe7ca86

File tree

4 files changed

+397
-2
lines changed

4 files changed

+397
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-e ../toolbox-core
2+
google-auth==2.43.0
3+
google-auth-oauthlib==1.2.1
4+
google-adk==1.20.0
5+
typing-extensions==4.12.2

packages/toolbox-adk/src/toolbox_adk/toolset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,14 @@ async def get_tools(
7777
tools = []
7878
if self._toolset_name:
7979
core_tools = await self._client.load_toolset(
80-
self._toolset_name, bound_params=self._bound_params
80+
self._toolset_name, bound_params=self._bound_params or {}
8181
)
8282
tools.extend(core_tools)
8383

8484
if self._tool_names:
8585
for name in self._tool_names:
8686
core_tool = await self._client.load_tool(
87-
name, bound_params=self._bound_params
87+
name, bound_params=self._bound_params or {}
8888
)
8989
tools.append(core_tool)
9090

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
"""Contains pytest fixtures that are accessible from all
16+
files present in the same directory."""
17+
18+
from __future__ import annotations
19+
20+
import os
21+
import platform
22+
import subprocess
23+
import tempfile
24+
import time
25+
from typing import Generator
26+
27+
import google
28+
import pytest_asyncio
29+
from google.auth import compute_engine
30+
from google.cloud import secretmanager, storage
31+
32+
33+
#### Define Utility Functions
34+
def get_env_var(key: str) -> str:
35+
"""Gets environment variables."""
36+
value = os.environ.get(key)
37+
if value is None:
38+
raise ValueError(f"Must set env var {key}")
39+
return value
40+
41+
42+
def access_secret_version(
43+
project_id: str, secret_id: str, version_id: str = "latest"
44+
) -> str:
45+
"""Accesses the payload of a given secret version from Secret Manager."""
46+
client = secretmanager.SecretManagerServiceClient()
47+
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
48+
response = client.access_secret_version(request={"name": name})
49+
return response.payload.data.decode("UTF-8")
50+
51+
52+
def create_tmpfile(content: str) -> str:
53+
"""Creates a temporary file with the given content."""
54+
with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile:
55+
tmpfile.write(content)
56+
return tmpfile.name
57+
58+
59+
def download_blob(
60+
bucket_name: str, source_blob_name: str, destination_file_name: str
61+
) -> None:
62+
"""Downloads a blob from a GCS bucket."""
63+
storage_client = storage.Client()
64+
65+
bucket = storage_client.bucket(bucket_name)
66+
blob = bucket.blob(source_blob_name)
67+
blob.download_to_filename(destination_file_name)
68+
69+
print(f"Blob {source_blob_name} downloaded to {destination_file_name}.")
70+
71+
72+
def get_toolbox_binary_url(toolbox_version: str) -> str:
73+
"""Constructs the GCS path to the toolbox binary."""
74+
os_system = platform.system().lower()
75+
arch = (
76+
"arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64"
77+
)
78+
return f"v{toolbox_version}/{os_system}/{arch}/toolbox"
79+
80+
81+
def get_auth_token(client_id: str) -> str:
82+
"""Retrieves an authentication token"""
83+
request = google.auth.transport.requests.Request()
84+
credentials = compute_engine.IDTokenCredentials(
85+
request=request,
86+
target_audience=client_id,
87+
use_metadata_identity_endpoint=True,
88+
)
89+
if not credentials.valid:
90+
credentials.refresh(request)
91+
return credentials.token
92+
93+
94+
#### Define Fixtures
95+
@pytest_asyncio.fixture(scope="session")
96+
def project_id() -> str:
97+
return get_env_var("GOOGLE_CLOUD_PROJECT")
98+
99+
100+
@pytest_asyncio.fixture(scope="session")
101+
def toolbox_version() -> str:
102+
return get_env_var("TOOLBOX_VERSION")
103+
104+
105+
@pytest_asyncio.fixture(scope="session")
106+
def tools_file_path(project_id: str) -> Generator[str]:
107+
"""Provides a temporary file path containing the tools manifest."""
108+
tools_manifest = access_secret_version(
109+
project_id=project_id,
110+
secret_id="sdk_testing_tools",
111+
version_id=os.environ.get("TOOLBOX_MANIFEST_VERSION", "latest"),
112+
)
113+
tools_file_path = create_tmpfile(tools_manifest)
114+
yield tools_file_path
115+
os.remove(tools_file_path)
116+
117+
118+
@pytest_asyncio.fixture(scope="session")
119+
def auth_token1(project_id: str) -> str:
120+
client_id = access_secret_version(
121+
project_id=project_id, secret_id="sdk_testing_client1"
122+
)
123+
return get_auth_token(client_id)
124+
125+
126+
@pytest_asyncio.fixture(scope="session")
127+
def auth_token2(project_id: str) -> str:
128+
client_id = access_secret_version(
129+
project_id=project_id, secret_id="sdk_testing_client2"
130+
)
131+
return get_auth_token(client_id)
132+
133+
134+
@pytest_asyncio.fixture(scope="session")
135+
def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]:
136+
"""Starts the toolbox server as a subprocess."""
137+
print("Downloading toolbox binary from gcs bucket...")
138+
source_blob_name = get_toolbox_binary_url(toolbox_version)
139+
download_blob("genai-toolbox", source_blob_name, "toolbox")
140+
141+
print("Toolbox binary downloaded successfully.")
142+
try:
143+
print("Opening toolbox server process...")
144+
# Make toolbox executable
145+
os.chmod("toolbox", 0o700)
146+
# Run toolbox binary
147+
toolbox_server = subprocess.Popen(
148+
["./toolbox", "--tools-file", tools_file_path]
149+
)
150+
151+
# Wait for server to start
152+
# Retry logic with a timeout
153+
for _ in range(5): # retries
154+
time.sleep(2)
155+
print("Checking if toolbox is successfully started...")
156+
if toolbox_server.poll() is None:
157+
print("Toolbox server started successfully.")
158+
break
159+
else:
160+
raise RuntimeError("Toolbox server failed to start after 5 retries.")
161+
except subprocess.CalledProcessError as e:
162+
print(e.stderr.decode("utf-8"))
163+
print(e.stdout.decode("utf-8"))
164+
raise RuntimeError(f"{e}\n\n{e.stderr.decode('utf-8')}") from e
165+
yield
166+
167+
# Clean up toolbox server
168+
toolbox_server.terminate()
169+
toolbox_server.wait(timeout=5)

0 commit comments

Comments
 (0)