chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# 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 os
|
||||
from unittest import mock
|
||||
|
||||
import google.adk
|
||||
from google.adk.integrations.bigquery.client import DP_USER_AGENT
|
||||
from google.adk.integrations.bigquery.client import get_bigquery_client
|
||||
from google.adk.integrations.bigquery.client import get_dataplex_catalog_client
|
||||
from google.adk.utils._telemetry_context import _is_visual_builder
|
||||
from google.api_core.gapic_v1 import client_info as gapic_client_info
|
||||
import google.auth
|
||||
from google.auth.exceptions import DefaultCredentialsError
|
||||
from google.cloud import dataplex_v1
|
||||
from google.cloud.bigquery import client as bigquery_client
|
||||
from google.oauth2.credentials import Credentials
|
||||
|
||||
|
||||
def test_bigquery_client_default():
|
||||
"""Test the default BigQuery client properties."""
|
||||
# Trigger the BigQuery client creation
|
||||
client = get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
)
|
||||
|
||||
# Verify that the client has the desired project set
|
||||
assert client.project == "test-gcp-project"
|
||||
assert client.location is None
|
||||
|
||||
|
||||
def test_bigquery_client_project_set_explicit():
|
||||
"""Test BigQuery client creation does not invoke default auth."""
|
||||
# Let's simulate that no environment variables are set, so that any project
|
||||
# set in there does not interfere with this test
|
||||
with mock.patch.dict(os.environ, {}, clear=True):
|
||||
with mock.patch.object(
|
||||
google.auth, "default", autospec=True
|
||||
) as mock_default_auth:
|
||||
# Simulate exception from default auth
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
# Trigger the BigQuery client creation
|
||||
client = get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
)
|
||||
|
||||
# If we are here that already means client creation did not call default
|
||||
# auth (otherwise we would have run into DefaultCredentialsError set
|
||||
# above). For the sake of explicitness, trivially assert that the default
|
||||
# auth was not called, and yet the project was set correctly
|
||||
mock_default_auth.assert_not_called()
|
||||
assert client.project == "test-gcp-project"
|
||||
|
||||
|
||||
def test_bigquery_client_project_set_with_default_auth():
|
||||
"""Test BigQuery client creation invokes default auth to set the project."""
|
||||
# Let's simulate that no environment variables are set, so that any project
|
||||
# set in there does not interfere with this test
|
||||
with mock.patch.dict(os.environ, {}, clear=True):
|
||||
with mock.patch.object(
|
||||
google.auth, "default", autospec=True
|
||||
) as mock_default_auth:
|
||||
# Simulate credentials
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
|
||||
# Simulate output of the default auth
|
||||
mock_default_auth.return_value = (mock_creds, "test-gcp-project")
|
||||
|
||||
# Trigger the BigQuery client creation
|
||||
client = get_bigquery_client(
|
||||
project=None,
|
||||
credentials=mock_creds,
|
||||
)
|
||||
|
||||
# Verify that default auth was called once to set the client project
|
||||
mock_default_auth.assert_called_once()
|
||||
assert client.project == "test-gcp-project"
|
||||
|
||||
|
||||
def test_bigquery_client_project_set_with_env():
|
||||
"""Test BigQuery client creation sets the project from environment variable."""
|
||||
# Let's simulate the project set in environment variables
|
||||
with mock.patch.dict(
|
||||
os.environ, {"GOOGLE_CLOUD_PROJECT": "test-gcp-project"}, clear=True
|
||||
):
|
||||
with mock.patch.object(
|
||||
google.auth, "default", autospec=True
|
||||
) as mock_default_auth:
|
||||
# Simulate exception from default auth
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
# Trigger the BigQuery client creation
|
||||
client = get_bigquery_client(
|
||||
project=None,
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
)
|
||||
|
||||
# If we are here that already means client creation did not call default
|
||||
# auth (otherwise we would have run into DefaultCredentialsError set
|
||||
# above). For the sake of explicitness, trivially assert that the default
|
||||
# auth was not called, and yet the project was set correctly
|
||||
mock_default_auth.assert_not_called()
|
||||
assert client.project == "test-gcp-project"
|
||||
|
||||
|
||||
def test_bigquery_client_user_agent_default():
|
||||
"""Test BigQuery client default user agent."""
|
||||
with mock.patch.object(
|
||||
bigquery_client, "Connection", autospec=True
|
||||
) as mock_connection:
|
||||
# Trigger the BigQuery client creation
|
||||
get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
)
|
||||
|
||||
# Verify that the tracking user agent was set
|
||||
client_info_arg = mock_connection.call_args[1].get("client_info")
|
||||
assert client_info_arg is not None
|
||||
expected_user_agents = {
|
||||
"adk-bigquery-tool",
|
||||
f"google-adk/{google.adk.__version__}",
|
||||
}
|
||||
actual_user_agents = set(client_info_arg.user_agent.split())
|
||||
assert expected_user_agents.issubset(actual_user_agents)
|
||||
|
||||
|
||||
def test_bigquery_client_user_agent_custom():
|
||||
"""Test BigQuery client custom user agent."""
|
||||
with mock.patch.object(
|
||||
bigquery_client, "Connection", autospec=True
|
||||
) as mock_connection:
|
||||
# Trigger the BigQuery client creation
|
||||
get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
user_agent="custom_user_agent",
|
||||
)
|
||||
|
||||
# Verify that the tracking user agent was set
|
||||
client_info_arg = mock_connection.call_args[1].get("client_info")
|
||||
assert client_info_arg is not None
|
||||
expected_user_agents = {
|
||||
"adk-bigquery-tool",
|
||||
f"google-adk/{google.adk.__version__}",
|
||||
"custom_user_agent",
|
||||
}
|
||||
actual_user_agents = set(client_info_arg.user_agent.split())
|
||||
assert expected_user_agents.issubset(actual_user_agents)
|
||||
|
||||
|
||||
def test_bigquery_client_user_agent_custom_list():
|
||||
"""Test BigQuery client custom user agent."""
|
||||
with mock.patch.object(
|
||||
bigquery_client, "Connection", autospec=True
|
||||
) as mock_connection:
|
||||
# Trigger the BigQuery client creation
|
||||
get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
user_agent=["custom_user_agent1", "custom_user_agent2"],
|
||||
)
|
||||
|
||||
# Verify that the tracking user agents were set
|
||||
client_info_arg = mock_connection.call_args[1].get("client_info")
|
||||
assert client_info_arg is not None
|
||||
expected_user_agents = {
|
||||
"adk-bigquery-tool",
|
||||
f"google-adk/{google.adk.__version__}",
|
||||
"custom_user_agent1",
|
||||
"custom_user_agent2",
|
||||
}
|
||||
actual_user_agents = set(client_info_arg.user_agent.split())
|
||||
assert expected_user_agents.issubset(actual_user_agents)
|
||||
|
||||
|
||||
def test_bigquery_client_user_agent_visual_builder():
|
||||
"""Test BigQuery client user agent when visual builder flag is set."""
|
||||
token = _is_visual_builder.set(True)
|
||||
try:
|
||||
with mock.patch.object(
|
||||
bigquery_client, "Connection", autospec=True
|
||||
) as mock_connection:
|
||||
# Trigger the BigQuery client creation
|
||||
get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
)
|
||||
|
||||
# Verify that the tracking user agent was set
|
||||
client_info_arg = mock_connection.call_args[1].get("client_info")
|
||||
assert client_info_arg is not None
|
||||
expected_user_agents = {
|
||||
"adk-bigquery-tool",
|
||||
f"google-adk/{google.adk.__version__}",
|
||||
"google-adk-visual-builder",
|
||||
}
|
||||
actual_user_agents = set(client_info_arg.user_agent.split())
|
||||
assert expected_user_agents.issubset(actual_user_agents)
|
||||
finally:
|
||||
_is_visual_builder.reset(token)
|
||||
|
||||
|
||||
def test_bigquery_client_location_custom():
|
||||
"""Test BigQuery client custom location."""
|
||||
# Trigger the BigQuery client creation
|
||||
client = get_bigquery_client(
|
||||
project="test-gcp-project",
|
||||
credentials=mock.create_autospec(Credentials, instance=True),
|
||||
location="us-central1",
|
||||
)
|
||||
|
||||
# Verify that the client has the desired project set
|
||||
assert client.project == "test-gcp-project"
|
||||
assert client.location == "us-central1"
|
||||
|
||||
|
||||
# Tests for Dataplex Catalog Client
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Mock the CatalogServiceClient class directly
|
||||
@mock.patch.object(dataplex_v1, "CatalogServiceClient", autospec=True)
|
||||
def test_dataplex_client_default(mock_catalog_service_client):
|
||||
"""Test get_dataplex_catalog_client with default user agent."""
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
|
||||
client = get_dataplex_catalog_client(credentials=mock_creds)
|
||||
|
||||
mock_catalog_service_client.assert_called_once()
|
||||
_, kwargs = mock_catalog_service_client.call_args
|
||||
|
||||
assert kwargs["credentials"] == mock_creds
|
||||
client_info = kwargs["client_info"]
|
||||
assert isinstance(client_info, gapic_client_info.ClientInfo)
|
||||
assert client_info.user_agent == DP_USER_AGENT
|
||||
|
||||
# Ensure the function returns the mock instance
|
||||
assert client == mock_catalog_service_client.return_value
|
||||
|
||||
|
||||
@mock.patch.object(dataplex_v1, "CatalogServiceClient", autospec=True)
|
||||
def test_dataplex_client_custom_user_agent_str(mock_catalog_service_client):
|
||||
"""Test get_dataplex_catalog_client with a custom user agent string."""
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
custom_ua = "catalog_ua/1.0"
|
||||
expected_ua = f"{DP_USER_AGENT} {custom_ua}"
|
||||
|
||||
get_dataplex_catalog_client(credentials=mock_creds, user_agent=custom_ua)
|
||||
|
||||
mock_catalog_service_client.assert_called_once()
|
||||
_, kwargs = mock_catalog_service_client.call_args
|
||||
client_info = kwargs["client_info"]
|
||||
assert client_info.user_agent == expected_ua
|
||||
|
||||
|
||||
@mock.patch.object(dataplex_v1, "CatalogServiceClient", autospec=True)
|
||||
def test_dataplex_client_custom_user_agent_list(mock_catalog_service_client):
|
||||
"""Test get_dataplex_catalog_client with a custom user agent list."""
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
custom_ua_list = ["catalog_ua", "catalog_ua_2.0"]
|
||||
expected_ua = f"{DP_USER_AGENT} {' '.join(custom_ua_list)}"
|
||||
|
||||
get_dataplex_catalog_client(credentials=mock_creds, user_agent=custom_ua_list)
|
||||
|
||||
mock_catalog_service_client.assert_called_once()
|
||||
_, kwargs = mock_catalog_service_client.call_args
|
||||
client_info = kwargs["client_info"]
|
||||
assert client_info.user_agent == expected_ua
|
||||
|
||||
|
||||
@mock.patch.object(dataplex_v1, "CatalogServiceClient", autospec=True)
|
||||
def test_dataplex_client_custom_user_agent_list_with_none(
|
||||
mock_catalog_service_client,
|
||||
):
|
||||
"""Test get_dataplex_catalog_client with a list containing None."""
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
custom_ua_list = ["catalog_ua", None, "catalog_ua_2.0"]
|
||||
expected_ua = f"{DP_USER_AGENT} catalog_ua catalog_ua_2.0"
|
||||
|
||||
get_dataplex_catalog_client(credentials=mock_creds, user_agent=custom_ua_list)
|
||||
|
||||
mock_catalog_service_client.assert_called_once()
|
||||
_, kwargs = mock_catalog_service_client.call_args
|
||||
client_info = kwargs["client_info"]
|
||||
assert client_info.user_agent == expected_ua
|
||||
@@ -0,0 +1,189 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from google.adk.integrations.bigquery import BigQueryCredentialsConfig
|
||||
# Mock the Google OAuth and API dependencies
|
||||
import google.auth.credentials
|
||||
import google.oauth2.credentials
|
||||
import pytest
|
||||
|
||||
|
||||
class TestBigQueryCredentials:
|
||||
"""Test suite for BigQueryCredentials configuration validation.
|
||||
|
||||
This class tests the credential configuration logic that ensures
|
||||
either existing credentials or client ID/secret pairs are provided.
|
||||
"""
|
||||
|
||||
def test_valid_credentials_object_auth_credentials(self):
|
||||
"""Test that providing valid Credentials object works correctly with
|
||||
google.auth.credentials.Credentials.
|
||||
|
||||
When a user already has valid OAuth credentials, they should be able
|
||||
to pass them directly without needing to provide client ID/secret.
|
||||
"""
|
||||
# Create a mock auth credentials object
|
||||
auth_creds = mock.create_autospec(
|
||||
google.auth.credentials.Credentials, instance=True
|
||||
)
|
||||
|
||||
config = BigQueryCredentialsConfig(credentials=auth_creds)
|
||||
|
||||
# Verify that the credentials are properly stored and attributes are extracted
|
||||
assert config.credentials == auth_creds
|
||||
assert config.client_secret is None
|
||||
assert config.scopes == [
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/dataplex.read-write",
|
||||
]
|
||||
|
||||
def test_valid_credentials_object_oauth2_credentials(self):
|
||||
"""Test that providing valid Credentials object works correctly with
|
||||
google.oauth2.credentials.Credentials.
|
||||
|
||||
When a user already has valid OAuth credentials, they should be able
|
||||
to pass them directly without needing to provide client ID/secret.
|
||||
"""
|
||||
# Create a mock oauth2 credentials object
|
||||
oauth2_creds = google.oauth2.credentials.Credentials(
|
||||
"test_token",
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
scopes=["https://www.googleapis.com/auth/calendar"],
|
||||
)
|
||||
|
||||
config = BigQueryCredentialsConfig(credentials=oauth2_creds)
|
||||
|
||||
# Verify that the credentials are properly stored and attributes are extracted
|
||||
assert config.credentials == oauth2_creds
|
||||
assert config.client_id == "test_client_id"
|
||||
assert config.client_secret == "test_client_secret"
|
||||
assert config.scopes == ["https://www.googleapis.com/auth/calendar"]
|
||||
|
||||
def test_valid_client_id_secret_pair_default_scope(self):
|
||||
"""Test that providing client ID and secret with default scope works.
|
||||
|
||||
This tests the scenario where users want to create new OAuth credentials
|
||||
from scratch using their application's client ID and secret and does not
|
||||
specify the scopes explicitly.
|
||||
"""
|
||||
config = BigQueryCredentialsConfig(
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
)
|
||||
|
||||
assert config.credentials is None
|
||||
assert config.client_id == "test_client_id"
|
||||
assert config.client_secret == "test_client_secret"
|
||||
assert config.scopes == [
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/dataplex.read-write",
|
||||
]
|
||||
|
||||
def test_valid_client_id_secret_pair_w_scope(self):
|
||||
"""Test that providing client ID and secret with explicit scopes works.
|
||||
|
||||
This tests the scenario where users want to create new OAuth credentials
|
||||
from scratch using their application's client ID and secret and does specify
|
||||
the scopes explicitly.
|
||||
"""
|
||||
config = BigQueryCredentialsConfig(
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
scopes=[
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/drive",
|
||||
],
|
||||
)
|
||||
|
||||
assert config.credentials is None
|
||||
assert config.client_id == "test_client_id"
|
||||
assert config.client_secret == "test_client_secret"
|
||||
assert config.scopes == [
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/drive",
|
||||
]
|
||||
|
||||
def test_valid_client_id_secret_pair_w_empty_scope(self):
|
||||
"""Test that providing client ID and secret with empty scope works.
|
||||
|
||||
This tests the corner case scenario where users want to create new OAuth
|
||||
credentials from scratch using their application's client ID and secret but
|
||||
specifies empty scope, in which case the default BQ scope is used.
|
||||
"""
|
||||
config = BigQueryCredentialsConfig(
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
scopes=[],
|
||||
)
|
||||
|
||||
assert config.credentials is None
|
||||
assert config.client_id == "test_client_id"
|
||||
assert config.client_secret == "test_client_secret"
|
||||
assert config.scopes == [
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/dataplex.read-write",
|
||||
]
|
||||
|
||||
def test_missing_client_secret_raises_error(self):
|
||||
"""Test that missing client secret raises appropriate validation error.
|
||||
|
||||
This ensures that incomplete OAuth configuration is caught early
|
||||
rather than failing during runtime.
|
||||
"""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"Must provide one of credentials, external_access_token_key, or"
|
||||
" client_id and client_secret pair"
|
||||
),
|
||||
):
|
||||
BigQueryCredentialsConfig(client_id="test_client_id")
|
||||
|
||||
def test_missing_client_id_raises_error(self):
|
||||
"""Test that missing client ID raises appropriate validation error."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"Must provide one of credentials, external_access_token_key, or"
|
||||
" client_id and client_secret pair"
|
||||
),
|
||||
):
|
||||
BigQueryCredentialsConfig(client_secret="test_client_secret")
|
||||
|
||||
def test_empty_configuration_raises_error(self):
|
||||
"""Test that completely empty configuration is rejected.
|
||||
|
||||
Users must provide either existing credentials or the components
|
||||
needed to create new ones.
|
||||
"""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"Must provide one of credentials, external_access_token_key, or"
|
||||
" client_id and client_secret pair"
|
||||
),
|
||||
):
|
||||
BigQueryCredentialsConfig()
|
||||
|
||||
def test_invalid_property_raises_error(self):
|
||||
"""Test BigQueryCredentialsConfig raises exception when setting invalid property."""
|
||||
with pytest.raises(ValueError):
|
||||
BigQueryCredentialsConfig(
|
||||
client_id="test_client_id",
|
||||
client_secret="test_client_secret",
|
||||
non_existent_field="some value",
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
# 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.
|
||||
|
||||
import pathlib
|
||||
from unittest import mock
|
||||
|
||||
from google.adk.integrations.bigquery import data_insights_tool
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case_file_path",
|
||||
[
|
||||
pytest.param("test_data/ask_data_insights_penguins_highest_mass.yaml"),
|
||||
],
|
||||
)
|
||||
@mock.patch.object(
|
||||
data_insights_tool._gda_stream_util, "get_gda_session", autospec=True
|
||||
)
|
||||
def test_ask_data_insights_pipeline_from_file(mock_get_session, case_file_path):
|
||||
"""Runs a full integration test for the ask_data_insights pipeline using data from a specific file."""
|
||||
# 1. Construct the full, absolute path to the data file
|
||||
full_path = pathlib.Path(__file__).parent / case_file_path
|
||||
|
||||
# 2. Load the test case data from the specified YAML file
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
case_data = yaml.safe_load(f)
|
||||
|
||||
# 3. Prepare the mock stream and expected output from the loaded data
|
||||
mock_stream_str = case_data["mock_api_stream"]
|
||||
fake_stream_lines = [
|
||||
line.encode("utf-8") for line in mock_stream_str.splitlines()
|
||||
]
|
||||
# Load the expected output as a list of dictionaries, not a single string
|
||||
expected_final_list = case_data["expected_output"]
|
||||
|
||||
# 4. Configure the mock for requests.post
|
||||
mock_session = mock.MagicMock()
|
||||
mock_response = mock.Mock()
|
||||
mock_response.iter_lines.return_value = fake_stream_lines
|
||||
# Add raise_for_status mock which is called in the updated code
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_session.post.return_value.__enter__.return_value = mock_response
|
||||
mock_get_session.return_value = (
|
||||
mock_session,
|
||||
"https://geminidataanalytics.mtls.googleapis.com",
|
||||
)
|
||||
|
||||
# 5. Call the function under test
|
||||
mock_creds = mock.Mock()
|
||||
mock_settings = mock.Mock()
|
||||
mock_settings.max_query_result_rows = 50
|
||||
result = data_insights_tool.ask_data_insights(
|
||||
project_id="test-project",
|
||||
user_query_with_context=case_data["user_question"],
|
||||
table_references=[],
|
||||
credentials=mock_creds,
|
||||
settings=mock_settings,
|
||||
)
|
||||
|
||||
# 6. Assert that the final list of dicts matches the expected output
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["response"] == expected_final_list
|
||||
mock_get_session.assert_called_once_with(mock_creds)
|
||||
mock_session.post.assert_called_once_with(
|
||||
"https://geminidataanalytics.mtls.googleapis.com/v1beta/projects/test-project/locations/global:chat",
|
||||
json={
|
||||
"project": "projects/test-project",
|
||||
"messages": [{"userMessage": {"text": case_data["user_question"]}}],
|
||||
"inlineContext": {
|
||||
"datasourceReferences": {"bq": {"tableReferences": []}},
|
||||
"systemInstruction": mock.ANY,
|
||||
"options": {"chart": {"image": {"noImage": {}}}},
|
||||
},
|
||||
"clientIdEnum": "GOOGLE_ADK",
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Goog-API-Client": "GOOGLE_ADK",
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(data_insights_tool._gda_stream_util, "get_stream")
|
||||
@mock.patch.object(
|
||||
data_insights_tool._gda_stream_util, "get_gda_session", autospec=True
|
||||
)
|
||||
def test_ask_data_insights_success(mock_get_session, mock_get_stream):
|
||||
"""Tests the success path of ask_data_insights using decorators."""
|
||||
# 1. Configure the behavior of the mocked functions
|
||||
mock_stream = [
|
||||
{"text": {"parts": ["response1"], "textType": "THOUGHT"}},
|
||||
{"text": {"parts": ["response2"], "textType": "FINAL_RESPONSE"}},
|
||||
]
|
||||
mock_get_stream.return_value = mock_stream
|
||||
mock_session = mock.MagicMock()
|
||||
mock_get_session.return_value = (
|
||||
mock_session,
|
||||
"https://geminidataanalytics.mtls.googleapis.com",
|
||||
)
|
||||
|
||||
# 2. Create mock inputs for the function call
|
||||
mock_creds = mock.Mock()
|
||||
mock_settings = mock.Mock()
|
||||
mock_settings.max_query_result_rows = 100
|
||||
|
||||
# 3. Call the function under test
|
||||
result = data_insights_tool.ask_data_insights(
|
||||
project_id="test-project",
|
||||
user_query_with_context="test query",
|
||||
table_references=[],
|
||||
credentials=mock_creds,
|
||||
settings=mock_settings,
|
||||
)
|
||||
|
||||
# 4. Assert the results are as expected
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["response"] == mock_stream
|
||||
mock_get_session.assert_called_once_with(mock_creds)
|
||||
mock_get_stream.assert_called_once_with(
|
||||
mock_session,
|
||||
"https://geminidataanalytics.mtls.googleapis.com/v1beta/projects/test-project/locations/global:chat",
|
||||
{
|
||||
"project": "projects/test-project",
|
||||
"messages": [{"userMessage": {"text": "test query"}}],
|
||||
"inlineContext": {
|
||||
"datasourceReferences": {"bq": {"tableReferences": []}},
|
||||
"systemInstruction": mock.ANY,
|
||||
"options": {"chart": {"image": {"noImage": {}}}},
|
||||
},
|
||||
"clientIdEnum": "GOOGLE_ADK",
|
||||
},
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"X-Goog-API-Client": "GOOGLE_ADK",
|
||||
},
|
||||
100,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(data_insights_tool._gda_stream_util, "get_stream")
|
||||
@mock.patch.object(
|
||||
data_insights_tool._gda_stream_util, "get_gda_session", autospec=True
|
||||
)
|
||||
def test_ask_data_insights_handles_exception(mock_get_session, mock_get_stream):
|
||||
"""Tests the exception path of ask_data_insights using decorators."""
|
||||
# 1. Configure one of the mocks to raise an error
|
||||
mock_get_stream.side_effect = Exception("API call failed!")
|
||||
mock_session = mock.MagicMock()
|
||||
mock_get_session.return_value = (
|
||||
mock_session,
|
||||
"https://geminidataanalytics.mtls.googleapis.com",
|
||||
)
|
||||
|
||||
# 2. Create mock inputs
|
||||
mock_creds = mock.Mock()
|
||||
mock_settings = mock.Mock()
|
||||
|
||||
# 3. Call the function
|
||||
result = data_insights_tool.ask_data_insights(
|
||||
project_id="test-project",
|
||||
user_query_with_context="test query",
|
||||
table_references=[],
|
||||
credentials=mock_creds,
|
||||
settings=mock_settings,
|
||||
)
|
||||
|
||||
# 4. Assert that the error was caught and formatted correctly
|
||||
assert result["status"] == "ERROR"
|
||||
assert "API call failed!" in result["error_details"]
|
||||
mock_get_session.assert_called_once_with(mock_creds)
|
||||
mock_get_stream.assert_called_once()
|
||||
@@ -0,0 +1,286 @@
|
||||
# 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 os
|
||||
from unittest import mock
|
||||
|
||||
from google.adk.integrations.bigquery import client as bq_client_lib
|
||||
from google.adk.integrations.bigquery import metadata_tool
|
||||
from google.adk.integrations.bigquery.config import BigQueryToolConfig
|
||||
import google.auth
|
||||
from google.auth.exceptions import DefaultCredentialsError
|
||||
from google.cloud import bigquery
|
||||
from google.oauth2.credentials import Credentials
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch.object(bigquery.Client, "list_datasets", autospec=True)
|
||||
@mock.patch.object(google.auth, "default", autospec=True)
|
||||
def test_list_dataset_ids_no_default_auth(
|
||||
mock_default_auth, mock_list_datasets
|
||||
):
|
||||
"""Test list_dataset_ids tool invocation involves no default auth."""
|
||||
project = "my_project_id"
|
||||
mock_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigQueryToolConfig()
|
||||
|
||||
# Simulate the behavior of default auth - on purpose throw exception when
|
||||
# the default auth is called
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
mock_list_datasets.return_value = [
|
||||
bigquery.DatasetReference(project, "dataset1"),
|
||||
bigquery.DatasetReference(project, "dataset2"),
|
||||
]
|
||||
result = metadata_tool.list_dataset_ids(
|
||||
project, mock_credentials, tool_settings
|
||||
)
|
||||
assert result == ["dataset1", "dataset2"]
|
||||
mock_default_auth.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch.object(bigquery.Client, "get_dataset", autospec=True)
|
||||
@mock.patch.object(google.auth, "default", autospec=True)
|
||||
def test_get_dataset_info_no_default_auth(mock_default_auth, mock_get_dataset):
|
||||
"""Test get_dataset_info tool invocation involves no default auth."""
|
||||
mock_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigQueryToolConfig()
|
||||
|
||||
# Simulate the behavior of default auth - on purpose throw exception when
|
||||
# the default auth is called
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
mock_get_dataset.return_value = mock.create_autospec(
|
||||
Credentials, instance=True
|
||||
)
|
||||
result = metadata_tool.get_dataset_info(
|
||||
"my_project_id", "my_dataset_id", mock_credentials, tool_settings
|
||||
)
|
||||
assert result != {
|
||||
"status": "ERROR",
|
||||
"error_details": "Your default credentials were not found",
|
||||
}
|
||||
mock_default_auth.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch.object(bigquery.Client, "list_tables", autospec=True)
|
||||
@mock.patch.object(google.auth, "default", autospec=True)
|
||||
def test_list_table_ids_no_default_auth(mock_default_auth, mock_list_tables):
|
||||
"""Test list_table_ids tool invocation involves no default auth."""
|
||||
project = "my_project_id"
|
||||
dataset = "my_dataset_id"
|
||||
dataset_ref = bigquery.DatasetReference(project, dataset)
|
||||
mock_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigQueryToolConfig()
|
||||
|
||||
# Simulate the behavior of default auth - on purpose throw exception when
|
||||
# the default auth is called
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
mock_list_tables.return_value = [
|
||||
bigquery.TableReference(dataset_ref, "table1"),
|
||||
bigquery.TableReference(dataset_ref, "table2"),
|
||||
]
|
||||
result = metadata_tool.list_table_ids(
|
||||
project, dataset, mock_credentials, tool_settings
|
||||
)
|
||||
assert result == ["table1", "table2"]
|
||||
mock_default_auth.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch.object(bigquery.Client, "get_table", autospec=True)
|
||||
@mock.patch.object(google.auth, "default", autospec=True)
|
||||
def test_get_table_info_no_default_auth(mock_default_auth, mock_get_table):
|
||||
"""Test get_table_info tool invocation involves no default auth."""
|
||||
mock_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigQueryToolConfig()
|
||||
|
||||
# Simulate the behavior of default auth - on purpose throw exception when
|
||||
# the default auth is called
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
mock_get_table.return_value = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.get_table_info(
|
||||
"my_project_id",
|
||||
"my_dataset_id",
|
||||
"my_table_id",
|
||||
mock_credentials,
|
||||
tool_settings,
|
||||
)
|
||||
assert result != {
|
||||
"status": "ERROR",
|
||||
"error_details": "Your default credentials were not found",
|
||||
}
|
||||
mock_default_auth.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
@mock.patch.object(bigquery.Client, "get_job", autospec=True)
|
||||
@mock.patch.object(google.auth, "default", autospec=True)
|
||||
def test_get_job_info_no_default_auth(mock_default_auth, mock_get_job):
|
||||
"""Test get_job_info tool invocation involves no default auth."""
|
||||
mock_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigQueryToolConfig()
|
||||
|
||||
# Simulate the behavior of default auth - on purpose throw exception when
|
||||
# the default auth is called
|
||||
mock_default_auth.side_effect = DefaultCredentialsError(
|
||||
"Your default credentials were not found"
|
||||
)
|
||||
|
||||
mock_get_job.return_value = mock.create_autospec(
|
||||
bigquery.QueryJob, instance=True
|
||||
)
|
||||
result = metadata_tool.get_job_info(
|
||||
"my_project_id",
|
||||
"my_job_id",
|
||||
mock_credentials,
|
||||
tool_settings,
|
||||
)
|
||||
assert result != {
|
||||
"status": "ERROR",
|
||||
"error_details": "Your default credentials were not found",
|
||||
}
|
||||
mock_default_auth.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(bq_client_lib, "get_bigquery_client", autospec=True)
|
||||
def test_list_dataset_ids_bq_client_creation(mock_get_bigquery_client):
|
||||
"""Test BigQuery client creation params during list_dataset_ids tool invocation."""
|
||||
bq_project = "my_project_id"
|
||||
bq_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
application_name = "my-agent"
|
||||
tool_settings = BigQueryToolConfig(application_name=application_name)
|
||||
|
||||
metadata_tool.list_dataset_ids(bq_project, bq_credentials, tool_settings)
|
||||
mock_get_bigquery_client.assert_called_once()
|
||||
assert len(mock_get_bigquery_client.call_args.kwargs) == 4
|
||||
assert mock_get_bigquery_client.call_args.kwargs["project"] == bq_project
|
||||
assert (
|
||||
mock_get_bigquery_client.call_args.kwargs["credentials"] == bq_credentials
|
||||
)
|
||||
assert mock_get_bigquery_client.call_args.kwargs["user_agent"] == [
|
||||
application_name,
|
||||
"list_dataset_ids",
|
||||
]
|
||||
|
||||
|
||||
@mock.patch.object(bq_client_lib, "get_bigquery_client", autospec=True)
|
||||
def test_get_dataset_info_bq_client_creation(mock_get_bigquery_client):
|
||||
"""Test BigQuery client creation params during get_dataset_info tool invocation."""
|
||||
bq_project = "my_project_id"
|
||||
bq_dataset = "my_dataset_id"
|
||||
bq_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
application_name = "my-agent"
|
||||
tool_settings = BigQueryToolConfig(application_name=application_name)
|
||||
|
||||
metadata_tool.get_dataset_info(
|
||||
bq_project, bq_dataset, bq_credentials, tool_settings
|
||||
)
|
||||
mock_get_bigquery_client.assert_called_once()
|
||||
assert len(mock_get_bigquery_client.call_args.kwargs) == 4
|
||||
assert mock_get_bigquery_client.call_args.kwargs["project"] == bq_project
|
||||
assert (
|
||||
mock_get_bigquery_client.call_args.kwargs["credentials"] == bq_credentials
|
||||
)
|
||||
assert mock_get_bigquery_client.call_args.kwargs["user_agent"] == [
|
||||
application_name,
|
||||
"get_dataset_info",
|
||||
]
|
||||
|
||||
|
||||
@mock.patch.object(bq_client_lib, "get_bigquery_client", autospec=True)
|
||||
def test_list_table_ids_bq_client_creation(mock_get_bigquery_client):
|
||||
"""Test BigQuery client creation params during list_table_ids tool invocation."""
|
||||
bq_project = "my_project_id"
|
||||
bq_dataset = "my_dataset_id"
|
||||
bq_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
application_name = "my-agent"
|
||||
tool_settings = BigQueryToolConfig(application_name=application_name)
|
||||
|
||||
metadata_tool.list_table_ids(
|
||||
bq_project, bq_dataset, bq_credentials, tool_settings
|
||||
)
|
||||
mock_get_bigquery_client.assert_called_once()
|
||||
assert len(mock_get_bigquery_client.call_args.kwargs) == 4
|
||||
assert mock_get_bigquery_client.call_args.kwargs["project"] == bq_project
|
||||
assert (
|
||||
mock_get_bigquery_client.call_args.kwargs["credentials"] == bq_credentials
|
||||
)
|
||||
assert mock_get_bigquery_client.call_args.kwargs["user_agent"] == [
|
||||
application_name,
|
||||
"list_table_ids",
|
||||
]
|
||||
|
||||
|
||||
@mock.patch.object(bq_client_lib, "get_bigquery_client", autospec=True)
|
||||
def test_get_table_info_bq_client_creation(mock_get_bigquery_client):
|
||||
"""Test BigQuery client creation params during get_table_info tool invocation."""
|
||||
bq_project = "my_project_id"
|
||||
bq_dataset = "my_dataset_id"
|
||||
bq_table = "my_table_id"
|
||||
bq_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
application_name = "my-agent"
|
||||
tool_settings = BigQueryToolConfig(application_name=application_name)
|
||||
|
||||
metadata_tool.get_table_info(
|
||||
bq_project, bq_dataset, bq_table, bq_credentials, tool_settings
|
||||
)
|
||||
mock_get_bigquery_client.assert_called_once()
|
||||
assert len(mock_get_bigquery_client.call_args.kwargs) == 4
|
||||
assert mock_get_bigquery_client.call_args.kwargs["project"] == bq_project
|
||||
assert (
|
||||
mock_get_bigquery_client.call_args.kwargs["credentials"] == bq_credentials
|
||||
)
|
||||
assert mock_get_bigquery_client.call_args.kwargs["user_agent"] == [
|
||||
application_name,
|
||||
"get_table_info",
|
||||
]
|
||||
|
||||
|
||||
@mock.patch.object(bq_client_lib, "get_bigquery_client", autospec=True)
|
||||
def test_get_job_info_bq_client_creation(mock_get_bigquery_client):
|
||||
"""Test BigQuery client creation params during get_table_info tool invocation."""
|
||||
bq_project = "my_project_id"
|
||||
bq_job_id = "my_job_id"
|
||||
bq_credentials = mock.create_autospec(Credentials, instance=True)
|
||||
application_name = "my-agent"
|
||||
tool_settings = BigQueryToolConfig(application_name=application_name)
|
||||
|
||||
metadata_tool.get_job_info(
|
||||
bq_project, bq_job_id, bq_credentials, tool_settings
|
||||
)
|
||||
mock_get_bigquery_client.assert_called_once()
|
||||
assert len(mock_get_bigquery_client.call_args.kwargs) == 4
|
||||
assert mock_get_bigquery_client.call_args.kwargs["project"] == bq_project
|
||||
assert (
|
||||
mock_get_bigquery_client.call_args.kwargs["credentials"] == bq_credentials
|
||||
)
|
||||
assert mock_get_bigquery_client.call_args.kwargs["user_agent"] == [
|
||||
application_name,
|
||||
"get_job_info",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
||||
# 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 sys
|
||||
from typing import Any
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from absl.testing import parameterized
|
||||
|
||||
# Mock google.genai and pydantic if not available, before importing google.adk modules
|
||||
try:
|
||||
import google.genai
|
||||
except ImportError:
|
||||
m = mock.MagicMock()
|
||||
m.__path__ = []
|
||||
sys.modules["google.genai"] = m
|
||||
sys.modules["google.genai.types"] = mock.MagicMock()
|
||||
sys.modules["google.genai.errors"] = mock.MagicMock()
|
||||
|
||||
try:
|
||||
import pydantic
|
||||
except ImportError:
|
||||
m_pydantic = mock.MagicMock()
|
||||
|
||||
class MockBaseModel:
|
||||
pass
|
||||
|
||||
m_pydantic.BaseModel = MockBaseModel
|
||||
sys.modules["pydantic"] = m_pydantic
|
||||
|
||||
try:
|
||||
import fastapi
|
||||
import fastapi.openapi.models
|
||||
except ImportError:
|
||||
m_fastapi = mock.MagicMock()
|
||||
m_fastapi.openapi.models = mock.MagicMock()
|
||||
sys.modules["fastapi"] = m_fastapi
|
||||
sys.modules["fastapi.openapi"] = mock.MagicMock()
|
||||
sys.modules["fastapi.openapi.models"] = mock.MagicMock()
|
||||
|
||||
|
||||
from google.adk.integrations.bigquery import search_tool
|
||||
from google.adk.integrations.bigquery.config import BigQueryToolConfig
|
||||
from google.api_core import exceptions as api_exceptions
|
||||
from google.auth.credentials import Credentials
|
||||
from google.cloud import dataplex_v1
|
||||
|
||||
|
||||
def _mock_creds():
|
||||
return mock.create_autospec(Credentials, instance=True)
|
||||
|
||||
|
||||
def _mock_settings(app_name: str | None = "test-app"):
|
||||
return BigQueryToolConfig(application_name=app_name)
|
||||
|
||||
|
||||
def _mock_search_entries_response(results: list[dict[str, Any]]):
|
||||
mock_response = mock.MagicMock(spec=dataplex_v1.SearchEntriesResponse)
|
||||
mock_results = []
|
||||
for r in results:
|
||||
mock_result = mock.create_autospec(
|
||||
dataplex_v1.SearchEntriesResult, instance=True
|
||||
)
|
||||
# Manually attach dataplex_entry since it's not visible in dir() of the proto class
|
||||
mock_entry = mock.create_autospec(dataplex_v1.Entry, instance=True)
|
||||
mock_result.dataplex_entry = mock_entry
|
||||
|
||||
mock_entry.name = r.get("name")
|
||||
mock_entry.entry_type = r.get("entry_type")
|
||||
mock_entry.update_time = r.get("update_time", "2026-01-14T05:00:00Z")
|
||||
|
||||
# Manually attach entry_source since it's not visible in dir() of the proto class
|
||||
mock_source = mock.create_autospec(dataplex_v1.EntrySource, instance=True)
|
||||
mock_entry.entry_source = mock_source
|
||||
|
||||
mock_source.display_name = r.get("display_name")
|
||||
mock_source.resource = r.get("linked_resource")
|
||||
mock_source.description = r.get("description")
|
||||
mock_source.location = r.get("location")
|
||||
mock_results.append(mock_result)
|
||||
mock_response.results = mock_results
|
||||
return mock_response
|
||||
|
||||
|
||||
class TestSearchCatalog(parameterized.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.mock_dataplex_client = mock.create_autospec(
|
||||
dataplex_v1.CatalogServiceClient, instance=True
|
||||
)
|
||||
|
||||
# Patch get_dataplex_catalog_client
|
||||
self.mock_get_dataplex_client = self.enter_context(
|
||||
mock.patch(
|
||||
"google.adk.integrations.bigquery.client.get_dataplex_catalog_client",
|
||||
autospec=True,
|
||||
)
|
||||
)
|
||||
self.mock_get_dataplex_client.return_value = self.mock_dataplex_client
|
||||
self.mock_dataplex_client.__enter__.return_value = self.mock_dataplex_client
|
||||
|
||||
# Patch SearchEntriesRequest
|
||||
self.mock_search_request = self.enter_context(
|
||||
mock.patch(
|
||||
"google.cloud.dataplex_v1.SearchEntriesRequest", autospec=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_search_catalog_success(self):
|
||||
"""Test the successful path of search_catalog."""
|
||||
creds = _mock_creds()
|
||||
settings = _mock_settings()
|
||||
prompt = "customer data"
|
||||
project_id = "test-project"
|
||||
location = "us"
|
||||
|
||||
mock_api_results = [{
|
||||
"name": "entry1",
|
||||
"entry_type": "TABLE",
|
||||
"display_name": "Cust Table",
|
||||
"linked_resource": (
|
||||
"//bigquery.googleapis.com/projects/p/datasets/d/tables/t1"
|
||||
),
|
||||
"description": "Table 1",
|
||||
"location": "us",
|
||||
}]
|
||||
self.mock_dataplex_client.search_entries.return_value = (
|
||||
_mock_search_entries_response(mock_api_results)
|
||||
)
|
||||
|
||||
result = search_tool.search_catalog(
|
||||
prompt=prompt,
|
||||
project_id=project_id,
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
location=location,
|
||||
)
|
||||
|
||||
with self.subTest("Test result content"):
|
||||
self.assertEqual(result["status"], "SUCCESS")
|
||||
self.assertLen(result["results"], 1)
|
||||
self.assertEqual(result["results"][0]["name"], "entry1")
|
||||
self.assertEqual(result["results"][0]["display_name"], "Cust Table")
|
||||
|
||||
with self.subTest("Test mock calls"):
|
||||
self.mock_get_dataplex_client.assert_called_once_with(
|
||||
credentials=creds, user_agent=["test-app", "search_catalog"]
|
||||
)
|
||||
|
||||
expected_query = (
|
||||
'(customer data) AND projectid="test-project" AND system=BIGQUERY'
|
||||
)
|
||||
self.mock_search_request.assert_called_once_with(
|
||||
name=f"projects/{project_id}/locations/us",
|
||||
query=expected_query,
|
||||
page_size=10,
|
||||
semantic_search=True,
|
||||
)
|
||||
self.mock_dataplex_client.search_entries.assert_called_once_with(
|
||||
request=self.mock_search_request.return_value
|
||||
)
|
||||
|
||||
def test_search_catalog_no_project_id(self):
|
||||
"""Test search_catalog with missing project_id."""
|
||||
result = search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="",
|
||||
credentials=_mock_creds(),
|
||||
settings=_mock_settings(),
|
||||
location="us",
|
||||
)
|
||||
self.assertEqual(result["status"], "ERROR")
|
||||
self.assertIn("project_id must be provided", result["error_details"])
|
||||
self.mock_get_dataplex_client.assert_not_called()
|
||||
|
||||
def test_search_catalog_api_error(self):
|
||||
"""Test search_catalog handling API exceptions."""
|
||||
self.mock_dataplex_client.search_entries.side_effect = (
|
||||
api_exceptions.BadRequest("Invalid query")
|
||||
)
|
||||
|
||||
result = search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="test-project",
|
||||
credentials=_mock_creds(),
|
||||
settings=_mock_settings(),
|
||||
location="us",
|
||||
)
|
||||
self.assertEqual(result["status"], "ERROR")
|
||||
self.assertIn(
|
||||
"Dataplex API Error: 400 Invalid query", result["error_details"]
|
||||
)
|
||||
|
||||
def test_search_catalog_other_exception(self):
|
||||
"""Test search_catalog handling unexpected exceptions."""
|
||||
self.mock_get_dataplex_client.side_effect = Exception(
|
||||
"Something went wrong"
|
||||
)
|
||||
|
||||
result = search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="test-project",
|
||||
credentials=_mock_creds(),
|
||||
settings=_mock_settings(),
|
||||
location="us",
|
||||
)
|
||||
self.assertEqual(result["status"], "ERROR")
|
||||
self.assertIn("Something went wrong", result["error_details"])
|
||||
|
||||
@parameterized.named_parameters(
|
||||
("project_filter", "p", ["proj1"], None, None, 'projectid="proj1"'),
|
||||
(
|
||||
"multi_project_filter",
|
||||
"p",
|
||||
["p1", "p2"],
|
||||
None,
|
||||
None,
|
||||
'(projectid="p1" OR projectid="p2")',
|
||||
),
|
||||
("type_filter", "p", None, None, ["TABLE"], 'type="TABLE"'),
|
||||
(
|
||||
"multi_type_filter",
|
||||
"p",
|
||||
None,
|
||||
None,
|
||||
["TABLE", "DATASET"],
|
||||
'(type="TABLE" OR type="DATASET")',
|
||||
),
|
||||
(
|
||||
"project_and_dataset_filters",
|
||||
"inventory",
|
||||
["proj1", "proj2"],
|
||||
["dsetA"],
|
||||
None,
|
||||
(
|
||||
'(projectid="proj1" OR projectid="proj2") AND'
|
||||
' (linked_resource:"//bigquery.googleapis.com/projects/proj1/datasets/dsetA/*"'
|
||||
' OR linked_resource:"//bigquery.googleapis.com/projects/proj2/datasets/dsetA/*")'
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_search_catalog_query_construction(
|
||||
self, prompt, project_ids, dataset_ids, types, expected_query_part
|
||||
):
|
||||
"""Test different query constructions based on filters."""
|
||||
search_tool.search_catalog(
|
||||
prompt=prompt,
|
||||
project_id="test-project",
|
||||
credentials=_mock_creds(),
|
||||
settings=_mock_settings(),
|
||||
location="us",
|
||||
project_ids_filter=project_ids,
|
||||
dataset_ids_filter=dataset_ids,
|
||||
types_filter=types,
|
||||
)
|
||||
|
||||
self.mock_search_request.assert_called_once()
|
||||
_, kwargs = self.mock_search_request.call_args
|
||||
query = kwargs["query"]
|
||||
|
||||
if prompt:
|
||||
assert f"({prompt})" in query
|
||||
assert "system=BIGQUERY" in query
|
||||
assert expected_query_part in query
|
||||
|
||||
def test_search_catalog_no_app_name(self):
|
||||
"""Test search_catalog when settings.application_name is None."""
|
||||
creds = _mock_creds()
|
||||
settings = _mock_settings(app_name=None)
|
||||
search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="test-project",
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
location="us",
|
||||
)
|
||||
|
||||
self.mock_get_dataplex_client.assert_called_once_with(
|
||||
credentials=creds, user_agent=[None, "search_catalog"]
|
||||
)
|
||||
|
||||
def test_search_catalog_multi_project_filter_semantic(self):
|
||||
"""Test semantic search with a multi-project filter."""
|
||||
creds = _mock_creds()
|
||||
settings = _mock_settings()
|
||||
prompt = "What datasets store user profiles?"
|
||||
project_id = "main-project"
|
||||
project_filters = ["user-data-proj", "shared-infra-proj"]
|
||||
location = "global"
|
||||
|
||||
self.mock_dataplex_client.search_entries.return_value = (
|
||||
_mock_search_entries_response([])
|
||||
)
|
||||
|
||||
search_tool.search_catalog(
|
||||
prompt=prompt,
|
||||
project_id=project_id,
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
location=location,
|
||||
project_ids_filter=project_filters,
|
||||
types_filter=["DATASET"],
|
||||
)
|
||||
|
||||
expected_query = (
|
||||
f"({prompt}) AND "
|
||||
'(projectid="user-data-proj" OR projectid="shared-infra-proj") AND '
|
||||
'type="DATASET" AND system=BIGQUERY'
|
||||
)
|
||||
self.mock_search_request.assert_called_once_with(
|
||||
name=f"projects/{project_id}/locations/{location}",
|
||||
query=expected_query,
|
||||
page_size=10,
|
||||
semantic_search=True,
|
||||
)
|
||||
self.mock_dataplex_client.search_entries.assert_called_once()
|
||||
|
||||
def test_search_catalog_natural_language_semantic(self):
|
||||
"""Test natural language prompts with semantic search enabled and check output."""
|
||||
creds = _mock_creds()
|
||||
settings = _mock_settings()
|
||||
prompt = "Find tables about football matches"
|
||||
project_id = "sports-analytics"
|
||||
location = "europe-west1"
|
||||
|
||||
# Mock the results that the API would return for this semantic query
|
||||
mock_api_results = [
|
||||
{
|
||||
"name": (
|
||||
"projects/sports-analytics/locations/europe-west1/entryGroups/@bigquery/entries/fb1"
|
||||
),
|
||||
"display_name": "uk_football_premiership",
|
||||
"entry_type": (
|
||||
"projects/655216118709/locations/global/entryTypes/bigquery-table"
|
||||
),
|
||||
"linked_resource": (
|
||||
"//bigquery.googleapis.com/projects/sports-analytics/datasets/uk/tables/premiership"
|
||||
),
|
||||
"description": "Stats for UK Premier League matches.",
|
||||
"location": "europe-west1",
|
||||
},
|
||||
{
|
||||
"name": (
|
||||
"projects/sports-analytics/locations/europe-west1/entryGroups/@bigquery/entries/fb2"
|
||||
),
|
||||
"display_name": "serie_a_matches",
|
||||
"entry_type": (
|
||||
"projects/655216118709/locations/global/entryTypes/bigquery-table"
|
||||
),
|
||||
"linked_resource": (
|
||||
"//bigquery.googleapis.com/projects/sports-analytics/datasets/italy/tables/serie_a"
|
||||
),
|
||||
"description": "Italian Serie A football results.",
|
||||
"location": "europe-west1",
|
||||
},
|
||||
]
|
||||
self.mock_dataplex_client.search_entries.return_value = (
|
||||
_mock_search_entries_response(mock_api_results)
|
||||
)
|
||||
|
||||
result = search_tool.search_catalog(
|
||||
prompt=prompt,
|
||||
project_id=project_id,
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
location=location,
|
||||
)
|
||||
|
||||
with self.subTest("Query Construction"):
|
||||
# Assert the request was made as expected
|
||||
expected_query = (
|
||||
f'({prompt}) AND projectid="{project_id}" AND system=BIGQUERY'
|
||||
)
|
||||
self.mock_search_request.assert_called_once_with(
|
||||
name=f"projects/{project_id}/locations/{location}",
|
||||
query=expected_query,
|
||||
page_size=10,
|
||||
semantic_search=True,
|
||||
)
|
||||
self.mock_dataplex_client.search_entries.assert_called_once()
|
||||
|
||||
with self.subTest("Response Processing"):
|
||||
# Assert the output is processed correctly
|
||||
self.assertEqual(result["status"], "SUCCESS")
|
||||
self.assertLen(result["results"], 2)
|
||||
self.assertEqual(
|
||||
result["results"][0]["display_name"], "uk_football_premiership"
|
||||
)
|
||||
self.assertEqual(result["results"][1]["display_name"], "serie_a_matches")
|
||||
self.assertIn("UK Premier League", result["results"][0]["description"])
|
||||
|
||||
def test_search_catalog_default_location(self):
|
||||
"""Test search_catalog fallback to global location when None is provided."""
|
||||
creds = _mock_creds()
|
||||
settings = _mock_settings()
|
||||
# settings.location is None by default
|
||||
|
||||
self.mock_dataplex_client.search_entries.return_value = (
|
||||
_mock_search_entries_response([])
|
||||
)
|
||||
|
||||
search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="test-project",
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
self.mock_search_request.assert_called_once()
|
||||
_, kwargs = self.mock_search_request.call_args
|
||||
name_arg = kwargs["name"]
|
||||
self.assertIn("locations/global", name_arg)
|
||||
|
||||
def test_search_catalog_settings_location(self):
|
||||
"""Test search_catalog uses settings.location when provided."""
|
||||
creds = _mock_creds()
|
||||
settings = BigQueryToolConfig(location="eu")
|
||||
|
||||
self.mock_dataplex_client.search_entries.return_value = (
|
||||
_mock_search_entries_response([])
|
||||
)
|
||||
|
||||
search_tool.search_catalog(
|
||||
prompt="test",
|
||||
project_id="test-project",
|
||||
credentials=creds,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
self.mock_search_request.assert_called_once()
|
||||
_, kwargs = self.mock_search_request.call_args
|
||||
name_arg = kwargs["name"]
|
||||
self.assertIn("locations/eu", name_arg)
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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 warnings
|
||||
|
||||
from google.adk.features._feature_registry import _WARNED_FEATURES
|
||||
from google.adk.integrations.bigquery.config import BigQueryToolConfig
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_warned_features():
|
||||
"""Reset warned features before each test."""
|
||||
_WARNED_FEATURES.clear()
|
||||
|
||||
|
||||
def test_bigquery_tool_config_invalid_property():
|
||||
"""Test BigQueryToolConfig raises exception when setting invalid property."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
):
|
||||
BigQueryToolConfig(non_existent_field="some value")
|
||||
|
||||
|
||||
def test_bigquery_tool_config_invalid_application_name():
|
||||
"""Test BigQueryToolConfig raises exception with invalid application name."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Application name should not contain spaces.",
|
||||
):
|
||||
BigQueryToolConfig(application_name="my agent")
|
||||
|
||||
|
||||
def test_bigquery_tool_config_max_query_result_rows_default():
|
||||
"""Test BigQueryToolConfig max_query_result_rows default value."""
|
||||
config = BigQueryToolConfig()
|
||||
assert config.max_query_result_rows == 50
|
||||
|
||||
|
||||
def test_bigquery_tool_config_max_query_result_rows_custom():
|
||||
"""Test BigQueryToolConfig max_query_result_rows custom value."""
|
||||
config = BigQueryToolConfig(max_query_result_rows=100)
|
||||
assert config.max_query_result_rows == 100
|
||||
|
||||
|
||||
def test_bigquery_tool_config_valid_maximum_bytes_billed():
|
||||
"""Test BigQueryToolConfig raises exception with valid max bytes billed."""
|
||||
config = BigQueryToolConfig(maximum_bytes_billed=10_485_760)
|
||||
assert config.maximum_bytes_billed == 10_485_760
|
||||
|
||||
|
||||
def test_bigquery_tool_config_invalid_maximum_bytes_billed():
|
||||
"""Test BigQueryToolConfig raises exception with invalid max bytes billed."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"In BigQuery on-demand pricing, charges are rounded up to the nearest"
|
||||
" MB, with a minimum 10 MB data processed per table referenced by the"
|
||||
" query, and with a minimum 10 MB data processed per query. So"
|
||||
" max_bytes_billed must be set >=10485760."
|
||||
),
|
||||
):
|
||||
BigQueryToolConfig(maximum_bytes_billed=10_485_759)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"labels",
|
||||
[
|
||||
pytest.param(
|
||||
{"environment": "test", "team": "data"},
|
||||
id="valid-labels",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
id="empty-labels",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
id="none-labels",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_bigquery_tool_config_valid_labels(labels):
|
||||
"""Test BigQueryToolConfig accepts valid labels."""
|
||||
config = BigQueryToolConfig(job_labels=labels)
|
||||
assert config.job_labels == labels
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("labels", "message"),
|
||||
[
|
||||
pytest.param(
|
||||
"invalid",
|
||||
"Input should be a valid dictionary",
|
||||
id="invalid-type",
|
||||
),
|
||||
pytest.param(
|
||||
{123: "value"},
|
||||
"Input should be a valid string",
|
||||
id="non-str-key",
|
||||
),
|
||||
pytest.param(
|
||||
{"key": 123},
|
||||
"Input should be a valid string",
|
||||
id="non-str-value",
|
||||
),
|
||||
pytest.param(
|
||||
{"": "value"},
|
||||
"Label keys cannot be empty",
|
||||
id="empty-label-key",
|
||||
),
|
||||
pytest.param(
|
||||
{"adk-bigquery-test": "value"},
|
||||
'Label key cannot start with "adk-bigquery-"',
|
||||
id="internal-label-key",
|
||||
),
|
||||
pytest.param(
|
||||
{f"key_{i}": "value" for i in range(21)},
|
||||
"Only up to 20 job labels can be provided",
|
||||
id="too-many-labels",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_bigquery_tool_config_invalid_labels(labels, message):
|
||||
"""Test BigQueryToolConfig raises an exception with invalid labels."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=message,
|
||||
):
|
||||
BigQueryToolConfig(job_labels=labels)
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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
|
||||
|
||||
from google.adk.integrations.bigquery import BigQueryCredentialsConfig
|
||||
from google.adk.integrations.bigquery import BigQueryToolset
|
||||
from google.adk.integrations.bigquery.config import BigQueryToolConfig
|
||||
from google.adk.tools.google_tool import GoogleTool
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigquery_toolset_tools_default():
|
||||
"""Test default BigQuery toolset.
|
||||
|
||||
This test verifies the behavior of the BigQuery toolset when no filter is
|
||||
specified.
|
||||
"""
|
||||
credentials_config = BigQueryCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigQueryToolset(
|
||||
credentials_config=credentials_config, bigquery_tool_config=None
|
||||
)
|
||||
# Verify that the tool config is initialized to default values.
|
||||
assert isinstance(toolset._tool_settings, BigQueryToolConfig) # pylint: disable=protected-access
|
||||
assert toolset._tool_settings.__dict__ == BigQueryToolConfig().__dict__ # pylint: disable=protected-access
|
||||
|
||||
tools = await toolset.get_tools()
|
||||
assert tools is not None
|
||||
|
||||
assert len(tools) == 11
|
||||
assert all([isinstance(tool, GoogleTool) for tool in tools])
|
||||
|
||||
expected_tool_names = set([
|
||||
"list_dataset_ids",
|
||||
"get_dataset_info",
|
||||
"list_table_ids",
|
||||
"get_table_info",
|
||||
"get_job_info",
|
||||
"execute_sql",
|
||||
"ask_data_insights",
|
||||
"forecast",
|
||||
"analyze_contribution",
|
||||
"detect_anomalies",
|
||||
"search_catalog",
|
||||
])
|
||||
actual_tool_names = set([tool.name for tool in tools])
|
||||
assert actual_tool_names == expected_tool_names
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"selected_tools",
|
||||
[
|
||||
pytest.param([], id="None"),
|
||||
pytest.param(
|
||||
["list_dataset_ids", "get_dataset_info"], id="dataset-metadata"
|
||||
),
|
||||
pytest.param(["list_table_ids", "get_table_info"], id="table-metadata"),
|
||||
pytest.param(["execute_sql"], id="query"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigquery_toolset_tools_selective(selected_tools):
|
||||
"""Test BigQuery toolset with filter.
|
||||
|
||||
This test verifies the behavior of the BigQuery toolset when filter is
|
||||
specified. A use case for this would be when the agent builder wants to
|
||||
use only a subset of the tools provided by the toolset.
|
||||
"""
|
||||
credentials_config = BigQueryCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigQueryToolset(
|
||||
credentials_config=credentials_config, tool_filter=selected_tools
|
||||
)
|
||||
tools = await toolset.get_tools()
|
||||
assert tools is not None
|
||||
|
||||
assert len(tools) == len(selected_tools)
|
||||
assert all([isinstance(tool, GoogleTool) for tool in tools])
|
||||
|
||||
expected_tool_names = set(selected_tools)
|
||||
actual_tool_names = set([tool.name for tool in tools])
|
||||
assert actual_tool_names == expected_tool_names
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("selected_tools", "returned_tools"),
|
||||
[
|
||||
pytest.param(["unknown"], [], id="all-unknown"),
|
||||
pytest.param(
|
||||
["unknown", "execute_sql"],
|
||||
["execute_sql"],
|
||||
id="mixed-known-unknown",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigquery_toolset_unknown_tool(selected_tools, returned_tools):
|
||||
"""Test BigQuery toolset with filter.
|
||||
|
||||
This test verifies the behavior of the BigQuery toolset when filter is
|
||||
specified with an unknown tool.
|
||||
"""
|
||||
credentials_config = BigQueryCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
|
||||
toolset = BigQueryToolset(
|
||||
credentials_config=credentials_config, tool_filter=selected_tools
|
||||
)
|
||||
|
||||
tools = await toolset.get_tools()
|
||||
assert tools is not None
|
||||
|
||||
assert len(tools) == len(returned_tools)
|
||||
assert all([isinstance(tool, GoogleTool) for tool in tools])
|
||||
|
||||
expected_tool_names = set(returned_tools)
|
||||
actual_tool_names = set([tool.name for tool in tools])
|
||||
assert actual_tool_names == expected_tool_names
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
|
||||
description: "Tests a full, realistic stream about finding the penguin island with the highest body mass."
|
||||
|
||||
user_question: "Penguins on which island have the highest average body mass?"
|
||||
|
||||
mock_api_stream: |
|
||||
[{
|
||||
"timestamp": "2025-07-17T17:25:28.231Z",
|
||||
"systemMessage": {
|
||||
"text": {
|
||||
"parts": [
|
||||
"Penguins on which island have the highest average body mass?"
|
||||
],
|
||||
"textType": "THOUGHT"
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"timestamp": "2025-07-17T17:25:31.171Z",
|
||||
"systemMessage": {
|
||||
"data": {
|
||||
"generatedSql": "SELECT island, AVG(body_mass_g) AS average_body_mass\nFROM `bigframes-dev-perf`.`bigframes_testing_eu`.`penguins`\nGROUP BY island;"
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"timestamp": "2025-07-17T17:25:32.664Z",
|
||||
"systemMessage": {
|
||||
"data": {
|
||||
"result": {
|
||||
"data": [
|
||||
{
|
||||
"island": "Biscoe",
|
||||
"average_body_mass": "4716.017964071853"
|
||||
},
|
||||
{
|
||||
"island": "Dream",
|
||||
"average_body_mass": "3712.9032258064512"
|
||||
},
|
||||
{
|
||||
"island": "Torgersen",
|
||||
"average_body_mass": "3706.3725490196075"
|
||||
}
|
||||
],
|
||||
"name": "average_body_mass_by_island",
|
||||
"schema": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "island",
|
||||
"type": "STRING",
|
||||
"mode": "NULLABLE"
|
||||
},
|
||||
{
|
||||
"name": "average_body_mass",
|
||||
"type": "FLOAT",
|
||||
"mode": "NULLABLE"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"timestamp": "2025-07-17T17:25:40.018Z",
|
||||
"systemMessage": {
|
||||
"text": {
|
||||
"parts": [
|
||||
"Penguins on Biscoe island have the highest average body mass, with an average of 4716.02g."
|
||||
],
|
||||
"textType": "FINAL_RESPONSE"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected_output:
|
||||
- text:
|
||||
parts:
|
||||
- 'Penguins on which island have the highest average body mass?'
|
||||
textType: THOUGHT
|
||||
- data:
|
||||
generatedSql: "SELECT island, AVG(body_mass_g) AS average_body_mass\nFROM `bigframes-dev-perf`.`bigframes_testing_eu`.`penguins`\nGROUP BY island;"
|
||||
- Data Retrieved:
|
||||
headers:
|
||||
- island
|
||||
- average_body_mass
|
||||
rows:
|
||||
- - Biscoe
|
||||
- '4716.017964071853'
|
||||
- - Dream
|
||||
- '3712.9032258064512'
|
||||
- - Torgersen
|
||||
- '3706.3725490196075'
|
||||
summary: Showing all 3 rows.
|
||||
- text:
|
||||
parts:
|
||||
- "Penguins on Biscoe island have the highest average body mass, with an average of 4716.02g."
|
||||
textType: FINAL_RESPONSE
|
||||
Reference in New Issue
Block a user