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,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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.tools.bigtable.bigtable_credentials import BIGTABLE_DEFAULT_SCOPE
|
||||
from google.adk.tools.bigtable.bigtable_credentials import BigtableCredentialsConfig
|
||||
from google.auth.credentials import Credentials
|
||||
import google.oauth2.credentials
|
||||
import pytest
|
||||
|
||||
|
||||
class TestBigtableCredentials:
|
||||
"""Test suite for Bigtable credentials configuration validation.
|
||||
|
||||
This class tests the credential configuration logic that ensures
|
||||
either existing credentials or client ID/secret pairs are provided.
|
||||
"""
|
||||
|
||||
def test_bigtable_credentials_config_client_id_secret(self):
|
||||
"""Test BigtableCredentialsConfig with client_id and client_secret.
|
||||
|
||||
Ensures that when client_id and client_secret are provided, the config
|
||||
object is created with the correct attributes.
|
||||
"""
|
||||
config = BigtableCredentialsConfig(client_id="abc", client_secret="def")
|
||||
assert config.client_id == "abc"
|
||||
assert config.client_secret == "def"
|
||||
assert config.scopes == BIGTABLE_DEFAULT_SCOPE
|
||||
assert config.credentials is None
|
||||
|
||||
def test_bigtable_credentials_config_existing_creds(self):
|
||||
"""Test BigtableCredentialsConfig with existing generic credentials.
|
||||
|
||||
Ensures that when a generic Credentials object is provided, it is
|
||||
stored correctly.
|
||||
"""
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
config = BigtableCredentialsConfig(credentials=mock_creds)
|
||||
assert config.credentials == mock_creds
|
||||
assert config.client_id is None
|
||||
assert config.client_secret is None
|
||||
|
||||
def test_bigtable_credentials_config_oauth2_creds(self):
|
||||
"""Test BigtableCredentialsConfig with existing OAuth2 credentials.
|
||||
|
||||
Ensures that when a google.oauth2.credentials.Credentials object is
|
||||
provided, the client_id, client_secret, and scopes are extracted
|
||||
from the credentials object.
|
||||
"""
|
||||
mock_creds = mock.create_autospec(
|
||||
google.oauth2.credentials.Credentials, instance=True
|
||||
)
|
||||
mock_creds.client_id = "oauth_client_id"
|
||||
mock_creds.client_secret = "oauth_client_secret"
|
||||
mock_creds.scopes = ["fake_scope"]
|
||||
config = BigtableCredentialsConfig(credentials=mock_creds)
|
||||
assert config.client_id == "oauth_client_id"
|
||||
assert config.client_secret == "oauth_client_secret"
|
||||
assert config.scopes == ["fake_scope"]
|
||||
|
||||
def test_bigtable_credentials_config_validation_errors(self):
|
||||
"""Test BigtableCredentialsConfig validation errors.
|
||||
|
||||
Ensures that ValueError is raised under the following conditions:
|
||||
- No arguments are provided.
|
||||
- Only client_id is provided.
|
||||
- Both credentials and client_id/client_secret are provided.
|
||||
"""
|
||||
with pytest.raises(ValueError):
|
||||
BigtableCredentialsConfig()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
BigtableCredentialsConfig(client_id="abc")
|
||||
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
with pytest.raises(ValueError):
|
||||
BigtableCredentialsConfig(
|
||||
credentials=mock_creds, client_id="abc", client_secret="def"
|
||||
)
|
||||
@@ -0,0 +1,272 @@
|
||||
# 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 logging
|
||||
from unittest import mock
|
||||
|
||||
from google.adk.tools.bigtable import client
|
||||
from google.adk.tools.bigtable import metadata_tool
|
||||
from google.auth.credentials import Credentials
|
||||
from google.cloud.bigtable import enums
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_client():
|
||||
with mock.patch.object(
|
||||
client, "get_bigtable_admin_client"
|
||||
) as mock_get_client:
|
||||
mock_client = mock.MagicMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
yield mock_get_client
|
||||
|
||||
|
||||
def test_list_instances(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_instance.instance_id = "test-instance"
|
||||
mock_get_client.return_value.list_instances.return_value = (
|
||||
[mock_instance],
|
||||
[],
|
||||
)
|
||||
|
||||
mock_instance.display_name = "Test Instance"
|
||||
mock_instance.state = enums.Instance.State.READY
|
||||
mock_instance.type_ = enums.Instance.Type.PRODUCTION
|
||||
mock_instance.labels = {"env": "test"}
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.list_instances(
|
||||
project_id="test-project", credentials=creds
|
||||
)
|
||||
expected_result = {
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"display_name": "Test Instance",
|
||||
"state": "READY",
|
||||
"type": "PRODUCTION",
|
||||
"labels": {"env": "test"},
|
||||
}
|
||||
assert result == {"status": "SUCCESS", "results": [expected_result]}
|
||||
|
||||
|
||||
def test_list_instances_failed_locations(mock_get_client):
|
||||
with mock.patch.object(logging, "warning") as mock_warning:
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_instance.instance_id = "test-instance"
|
||||
failed_locations = ["us-west1-a"]
|
||||
mock_get_client.return_value.list_instances.return_value = (
|
||||
[mock_instance],
|
||||
failed_locations,
|
||||
)
|
||||
|
||||
mock_instance.display_name = "Test Instance"
|
||||
mock_instance.state = enums.Instance.State.READY
|
||||
mock_instance.type_ = enums.Instance.Type.PRODUCTION
|
||||
mock_instance.labels = {"env": "test"}
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.list_instances(
|
||||
project_id="test-project", credentials=creds
|
||||
)
|
||||
expected_result = {
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"display_name": "Test Instance",
|
||||
"state": "READY",
|
||||
"type": "PRODUCTION",
|
||||
"labels": {"env": "test"},
|
||||
}
|
||||
assert result == {"status": "SUCCESS", "results": [expected_result]}
|
||||
mock_warning.assert_called_once_with(
|
||||
"Failed to list instances from the following locations: %s",
|
||||
failed_locations,
|
||||
)
|
||||
|
||||
|
||||
def test_get_instance_info(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_get_client.return_value.instance.return_value = mock_instance
|
||||
mock_instance.instance_id = "test-instance"
|
||||
mock_instance.display_name = "Test Instance"
|
||||
mock_instance.state = enums.Instance.State.READY
|
||||
mock_instance.type_ = enums.Instance.Type.PRODUCTION
|
||||
mock_instance.labels = {"env": "test"}
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.get_instance_info(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
credentials=creds,
|
||||
)
|
||||
expected_result = {
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"display_name": "Test Instance",
|
||||
"state": "READY",
|
||||
"type": "PRODUCTION",
|
||||
"labels": {"env": "test"},
|
||||
}
|
||||
assert result == {"status": "SUCCESS", "results": expected_result}
|
||||
mock_instance.reload.assert_called_once()
|
||||
|
||||
|
||||
def test_list_tables(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_get_client.return_value.instance.return_value = mock_instance
|
||||
mock_table = mock.MagicMock()
|
||||
mock_table.table_id = "test-table"
|
||||
mock_table.name = (
|
||||
"projects/test-project/instances/test-instance/tables/test-table"
|
||||
)
|
||||
mock_instance.list_tables.return_value = [mock_table]
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.list_tables(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
credentials=creds,
|
||||
)
|
||||
expected_result = [{
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"table_id": "test-table",
|
||||
"table_name": (
|
||||
"projects/test-project/instances/test-instance/tables/test-table"
|
||||
),
|
||||
}]
|
||||
assert result == {"status": "SUCCESS", "results": expected_result}
|
||||
|
||||
|
||||
def test_get_table_info(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_instance.instance_id = "test-instance"
|
||||
mock_get_client.return_value.instance.return_value = mock_instance
|
||||
mock_table = mock.MagicMock()
|
||||
mock_instance.table.return_value = mock_table
|
||||
mock_table.table_id = "test-table"
|
||||
mock_table.list_column_families.return_value = {"cf1": mock.MagicMock()}
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.get_table_info(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
table_id="test-table",
|
||||
credentials=creds,
|
||||
)
|
||||
expected_result = {
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"table_id": "test-table",
|
||||
"column_families": ["cf1"],
|
||||
}
|
||||
assert result == {"status": "SUCCESS", "results": expected_result}
|
||||
|
||||
|
||||
def test_list_clusters(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_get_client.return_value.instance.return_value = mock_instance
|
||||
mock_cluster = mock.MagicMock()
|
||||
mock_cluster.cluster_id = "test-cluster"
|
||||
mock_cluster.name = (
|
||||
"projects/test-project/instances/test-instance/clusters/test-cluster"
|
||||
)
|
||||
mock_cluster.state = enums.Cluster.State.READY
|
||||
mock_cluster.serve_nodes = 3
|
||||
mock_cluster.default_storage_type = enums.StorageType.SSD
|
||||
mock_cluster.location_id = "us-central1-a"
|
||||
mock_instance.list_clusters.return_value = ([mock_cluster], [])
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.list_clusters(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
credentials=creds,
|
||||
)
|
||||
expected_result = [{
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"cluster_id": "test-cluster",
|
||||
"cluster_name": mock_cluster.name,
|
||||
"state": "READY",
|
||||
"serve_nodes": 3,
|
||||
"default_storage_type": "SSD",
|
||||
"location_id": "us-central1-a",
|
||||
}]
|
||||
assert result == {"status": "SUCCESS", "results": expected_result}
|
||||
|
||||
|
||||
def test_list_clusters_error(mock_get_client):
|
||||
mock_get_client.side_effect = Exception("test-error")
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.list_clusters(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
credentials=creds,
|
||||
)
|
||||
assert result == {
|
||||
"status": "ERROR",
|
||||
"error_details": "Exception('test-error')",
|
||||
}
|
||||
|
||||
|
||||
def test_get_cluster_info(mock_get_client):
|
||||
mock_instance = mock.MagicMock()
|
||||
mock_get_client.return_value.instance.return_value = mock_instance
|
||||
mock_cluster = mock.MagicMock()
|
||||
mock_instance.cluster.return_value = mock_cluster
|
||||
mock_cluster.cluster_id = "test-cluster"
|
||||
mock_cluster.state = enums.Cluster.State.READY
|
||||
mock_cluster.serve_nodes = 3
|
||||
mock_cluster.default_storage_type = enums.StorageType.SSD
|
||||
mock_cluster.location_id = "us-central1-a"
|
||||
mock_cluster.min_serve_nodes = 3
|
||||
mock_cluster.max_serve_nodes = 10
|
||||
mock_cluster.cpu_utilization_percent = 50
|
||||
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.get_cluster_info(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
cluster_id="test-cluster",
|
||||
credentials=creds,
|
||||
)
|
||||
expected_results = {
|
||||
"project_id": "test-project",
|
||||
"instance_id": "test-instance",
|
||||
"cluster_id": "test-cluster",
|
||||
"state": "READY",
|
||||
"serve_nodes": 3,
|
||||
"default_storage_type": "SSD",
|
||||
"location_id": "us-central1-a",
|
||||
"min_serve_nodes": 3,
|
||||
"max_serve_nodes": 10,
|
||||
"cpu_utilization_percent": 50,
|
||||
}
|
||||
assert result == {"status": "SUCCESS", "results": expected_results}
|
||||
mock_cluster.reload.assert_called_once()
|
||||
|
||||
|
||||
def test_get_cluster_info_error(mock_get_client):
|
||||
mock_get_client.side_effect = Exception("test-error")
|
||||
creds = mock.create_autospec(Credentials, instance=True)
|
||||
result = metadata_tool.get_cluster_info(
|
||||
project_id="test-project",
|
||||
instance_id="test-instance",
|
||||
cluster_id="test-cluster",
|
||||
credentials=creds,
|
||||
)
|
||||
assert result == {
|
||||
"status": "ERROR",
|
||||
"error_details": "Exception('test-error')",
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from google.adk.tools.bigtable import client
|
||||
from google.adk.tools.bigtable.query_tool import execute_sql
|
||||
from google.adk.tools.bigtable.settings import BigtableToolSettings
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.auth.credentials import Credentials
|
||||
from google.cloud.bigtable.data.execute_query import ExecuteQueryIterator
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"query",
|
||||
"settings",
|
||||
"parameters",
|
||||
"parameter_types",
|
||||
"execute_query_side_effect",
|
||||
"iterator_yield_values",
|
||||
"expected_result",
|
||||
),
|
||||
[
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
BigtableToolSettings(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[{"col1": "val1", "col2": 123}],
|
||||
{"status": "SUCCESS", "rows": [{"col1": "val1", "col2": 123}]},
|
||||
id="basic",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
BigtableToolSettings(max_query_result_rows=1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[{"col1": "val1"}, {"col1": "val2"}],
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"rows": [{"col1": "val1"}],
|
||||
"result_is_likely_truncated": True,
|
||||
},
|
||||
id="truncated",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
BigtableToolSettings(),
|
||||
None,
|
||||
None,
|
||||
Exception("Test error"),
|
||||
None,
|
||||
{"status": "ERROR", "error_details": "Test error"},
|
||||
id="error",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table WHERE col1 = @param1",
|
||||
BigtableToolSettings(),
|
||||
{"param1": "val1"},
|
||||
{"param1": "string"},
|
||||
None,
|
||||
[{"col1": "val1"}],
|
||||
{"status": "SUCCESS", "rows": [{"col1": "val1"}]},
|
||||
id="with_parameters",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table WHERE 1=0",
|
||||
BigtableToolSettings(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[],
|
||||
{"status": "SUCCESS", "rows": []},
|
||||
id="empty_results",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
BigtableToolSettings(max_query_result_rows=10),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"rows": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
|
||||
},
|
||||
id="multiple_rows",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
[{"id": i} for i in range(51)],
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"rows": [{"id": i} for i in range(50)],
|
||||
"result_is_likely_truncated": True,
|
||||
},
|
||||
id="settings_none_uses_default",
|
||||
),
|
||||
pytest.param(
|
||||
"SELECT * FROM my_table",
|
||||
BigtableToolSettings(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Exception("Iteration failed"),
|
||||
{"status": "ERROR", "error_details": "Iteration failed"},
|
||||
id="iteration_error_calls_close",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_execute_sql(
|
||||
query,
|
||||
settings,
|
||||
parameters,
|
||||
parameter_types,
|
||||
execute_query_side_effect,
|
||||
iterator_yield_values,
|
||||
expected_result,
|
||||
):
|
||||
"""Test execute_sql tool functionality."""
|
||||
project = "my_project"
|
||||
instance_id = "my_instance"
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_context = mock.create_autospec(ToolContext, instance=True)
|
||||
|
||||
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
|
||||
mock_client = mock.MagicMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
if execute_query_side_effect:
|
||||
mock_client.execute_query.side_effect = execute_query_side_effect
|
||||
else:
|
||||
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
|
||||
mock_client.execute_query.return_value = mock_iterator
|
||||
|
||||
if isinstance(iterator_yield_values, Exception):
|
||||
|
||||
def raise_error():
|
||||
yield mock.MagicMock()
|
||||
raise iterator_yield_values
|
||||
|
||||
mock_iterator.__iter__.side_effect = raise_error
|
||||
else:
|
||||
mock_rows = []
|
||||
for fields in iterator_yield_values:
|
||||
mock_row = mock.MagicMock()
|
||||
mock_row.fields = fields
|
||||
mock_rows.append(mock_row)
|
||||
mock_iterator.__iter__.return_value = mock_rows
|
||||
|
||||
result = await execute_sql(
|
||||
project_id=project,
|
||||
instance_id=instance_id,
|
||||
credentials=credentials,
|
||||
query=query,
|
||||
settings=settings,
|
||||
tool_context=tool_context,
|
||||
parameters=parameters,
|
||||
parameter_types=parameter_types,
|
||||
)
|
||||
|
||||
if expected_result["status"] == "ERROR":
|
||||
assert result["status"] == "ERROR"
|
||||
assert expected_result["error_details"] in result["error_details"]
|
||||
else:
|
||||
assert result == expected_result
|
||||
|
||||
if not execute_query_side_effect:
|
||||
mock_client.execute_query.assert_called_once_with(
|
||||
query=query,
|
||||
instance_id=instance_id,
|
||||
parameters=parameters,
|
||||
parameter_types=parameter_types,
|
||||
view_parameters=None,
|
||||
)
|
||||
mock_iterator.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sql_row_value_circular_reference_fallback():
|
||||
"""Test execute_sql converts circular row values to strings."""
|
||||
project = "my_project"
|
||||
instance_id = "my_instance"
|
||||
query = "SELECT * FROM my_table"
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_context = mock.create_autospec(ToolContext, instance=True)
|
||||
|
||||
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
|
||||
mock_client = mock.MagicMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
|
||||
mock_client.execute_query.return_value = mock_iterator
|
||||
circular_value = []
|
||||
circular_value.append(circular_value)
|
||||
mock_row = mock.MagicMock()
|
||||
mock_row.fields = {"col1": circular_value}
|
||||
mock_iterator.__iter__.return_value = [mock_row]
|
||||
|
||||
result = await execute_sql(
|
||||
project_id=project,
|
||||
instance_id=instance_id,
|
||||
credentials=credentials,
|
||||
query=query,
|
||||
settings=BigtableToolSettings(),
|
||||
tool_context=tool_context,
|
||||
)
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["rows"][0]["col1"] == str(circular_value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sql_with_view_parameters():
|
||||
"""Test execute_sql with _view_parameters passed."""
|
||||
project = "my_project"
|
||||
instance_id = "my_instance"
|
||||
query = "SELECT * FROM my_table"
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_context = mock.create_autospec(ToolContext, instance=True)
|
||||
view_parameters = {"user_id": "test-user-123"}
|
||||
|
||||
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
|
||||
mock_client = mock.MagicMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
|
||||
mock_client.execute_query.return_value = mock_iterator
|
||||
mock_iterator.__iter__.return_value = []
|
||||
|
||||
result = await execute_sql(
|
||||
project_id=project,
|
||||
instance_id=instance_id,
|
||||
credentials=credentials,
|
||||
query=query,
|
||||
settings=BigtableToolSettings(),
|
||||
tool_context=tool_context,
|
||||
_view_parameters=view_parameters,
|
||||
)
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
mock_client.execute_query.assert_called_once_with(
|
||||
query=query,
|
||||
instance_id=instance_id,
|
||||
parameters=None,
|
||||
parameter_types=None,
|
||||
view_parameters=view_parameters,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sql_with_multiple_view_parameters():
|
||||
"""Test execute_sql with multiple view_parameters of different names."""
|
||||
project = "my_project"
|
||||
instance_id = "my_instance"
|
||||
query = "SELECT * FROM my_table"
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_context = mock.create_autospec(ToolContext, instance=True)
|
||||
view_parameters = {
|
||||
"user_id": "test-user-123",
|
||||
"tenant_id": "tenant-xyz",
|
||||
"role": "admin",
|
||||
}
|
||||
|
||||
with mock.patch.object(client, "get_bigtable_data_client") as mock_get_client:
|
||||
mock_client = mock.MagicMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_iterator = mock.create_autospec(ExecuteQueryIterator, instance=True)
|
||||
mock_client.execute_query.return_value = mock_iterator
|
||||
mock_iterator.__iter__.return_value = []
|
||||
|
||||
result = await execute_sql(
|
||||
project_id=project,
|
||||
instance_id=instance_id,
|
||||
credentials=credentials,
|
||||
query=query,
|
||||
settings=BigtableToolSettings(),
|
||||
tool_context=tool_context,
|
||||
_view_parameters=view_parameters,
|
||||
)
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
mock_client.execute_query.assert_called_once_with(
|
||||
query=query,
|
||||
instance_id=instance_id,
|
||||
parameters=None,
|
||||
parameter_types=None,
|
||||
view_parameters=view_parameters,
|
||||
)
|
||||
@@ -0,0 +1,340 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from google.adk.agents.context import Context
|
||||
from google.adk.agents.invocation_context import InvocationContext
|
||||
from google.adk.sessions.session import Session
|
||||
from google.adk.tools.bigtable import BigtableCredentialsConfig
|
||||
from google.adk.tools.bigtable import metadata_tool
|
||||
from google.adk.tools.bigtable import query_tool
|
||||
from google.adk.tools.bigtable.bigtable_toolset import BigtableParameterizedViewTool
|
||||
from google.adk.tools.bigtable.bigtable_toolset import BigtableToolset
|
||||
from google.adk.tools.bigtable.bigtable_toolset import DEFAULT_BIGTABLE_TOOL_NAME_PREFIX
|
||||
from google.adk.tools.bigtable.settings import BigtableToolSettings
|
||||
from google.adk.tools.google_tool import GoogleTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.auth.credentials import Credentials
|
||||
import pytest
|
||||
|
||||
|
||||
def test_bigtable_toolset_name_prefix():
|
||||
"""Test Bigtable toolset name prefix."""
|
||||
credentials_config = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigtableToolset(credentials_config=credentials_config)
|
||||
assert toolset.tool_name_prefix == DEFAULT_BIGTABLE_TOOL_NAME_PREFIX
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_toolset_tools_default():
|
||||
"""Test default Bigtable toolset."""
|
||||
credentials_config = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigtableToolset(credentials_config=credentials_config)
|
||||
|
||||
tools = await toolset.get_tools()
|
||||
assert tools is not None
|
||||
|
||||
assert len(tools) == 7
|
||||
assert all([isinstance(tool, GoogleTool) for tool in tools])
|
||||
|
||||
expected_tool_names = set([
|
||||
"list_instances",
|
||||
"get_instance_info",
|
||||
"list_tables",
|
||||
"get_table_info",
|
||||
"execute_sql",
|
||||
"list_clusters",
|
||||
"get_cluster_info",
|
||||
])
|
||||
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_instances", "get_instance_info"], id="instance-metadata"
|
||||
),
|
||||
pytest.param(["list_tables", "get_table_info"], id="table-metadata"),
|
||||
pytest.param(["execute_sql"], id="query"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_toolset_tools_selective(selected_tools):
|
||||
"""Test Bigtable toolset with filter.
|
||||
|
||||
This test verifies the behavior of the Bigtable 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 = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigtableToolset(
|
||||
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_bigtable_toolset_unknown_tool(selected_tools, returned_tools):
|
||||
"""Test Bigtable toolset with filter.
|
||||
|
||||
This test verifies the behavior of the Bigtable toolset when filter is
|
||||
specified with an unknown tool.
|
||||
"""
|
||||
credentials_config = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
|
||||
toolset = BigtableToolset(
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_toolset_query_tool_wrapped():
|
||||
"""Test that execute_sql is wrapped in BigtableQueryTool."""
|
||||
credentials_config = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigtableToolset(credentials_config=credentials_config)
|
||||
|
||||
tools = await toolset.get_tools()
|
||||
query_tools = [tool for tool in tools if tool.name == "execute_sql"]
|
||||
assert len(query_tools) == 1
|
||||
|
||||
parameterized_tools = [
|
||||
tool for tool in tools if tool.name == "execute_sql_parameterized"
|
||||
]
|
||||
assert len(parameterized_tools) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_toolset_query_tool_wrapped_custom_mapping():
|
||||
"""Test that BigtableParameterizedViewTool accepts custom mapping."""
|
||||
credentials_config = BigtableCredentialsConfig(
|
||||
client_id="abc", client_secret="def"
|
||||
)
|
||||
toolset = BigtableToolset(
|
||||
credentials_config=credentials_config,
|
||||
view_parameter_names=["user_id"],
|
||||
)
|
||||
|
||||
tools = await toolset.get_tools()
|
||||
parameterized_tools = [
|
||||
tool for tool in tools if tool.name == "execute_sql_parameterized"
|
||||
]
|
||||
assert len(parameterized_tools) == 1
|
||||
tool = parameterized_tools[0]
|
||||
assert isinstance(tool, BigtableParameterizedViewTool)
|
||||
assert tool.view_parameter_names == ["user_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_parameterized_view_tool_execution():
|
||||
"""Test that BigtableParameterizedViewTool maps attributes from tool_context to view_parameters."""
|
||||
|
||||
# Define a dummy function to wrap that has '_view_parameters' parameter
|
||||
def mock_execute_sql(_view_parameters=None):
|
||||
return {"status": "SUCCESS", "_view_parameters": _view_parameters}
|
||||
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool_settings = BigtableToolSettings()
|
||||
tool_context = mock.create_autospec(ToolContext, instance=True)
|
||||
tool_context.user_id = "test-user-123"
|
||||
|
||||
# Create tool with custom mapping
|
||||
tool = BigtableParameterizedViewTool(
|
||||
func=mock_execute_sql,
|
||||
view_parameter_names=["user_id"],
|
||||
)
|
||||
|
||||
# Run the tool
|
||||
res = await tool._run_async_with_credential(
|
||||
credentials=credentials,
|
||||
tool_settings=tool_settings,
|
||||
args={},
|
||||
tool_context=tool_context,
|
||||
)
|
||||
|
||||
assert res == {
|
||||
"status": "SUCCESS",
|
||||
"_view_parameters": {"user_id": "test-user-123"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_parameterized_view_tool_login_flow():
|
||||
"""Test showing how user_id is updated in the session/context after login."""
|
||||
|
||||
def mock_execute_sql(_view_parameters=None):
|
||||
return {"status": "SUCCESS", "_view_parameters": _view_parameters}
|
||||
|
||||
session = Session(id="session-1", app_name="test-app", user_id="anonymous")
|
||||
|
||||
invocation_context = mock.create_autospec(InvocationContext, instance=True)
|
||||
invocation_context.session = session
|
||||
type(invocation_context).user_id = property(lambda self: self.session.user_id)
|
||||
|
||||
tool_context = Context(invocation_context=invocation_context)
|
||||
assert tool_context.user_id == "anonymous"
|
||||
|
||||
# Simulate login by updating user_id on session
|
||||
session.user_id = "authenticated-user-999"
|
||||
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool = BigtableParameterizedViewTool(
|
||||
func=mock_execute_sql,
|
||||
view_parameter_names=["user_id"],
|
||||
)
|
||||
|
||||
res = await tool._run_async_with_credential(
|
||||
credentials=credentials,
|
||||
tool_settings=BigtableToolSettings(),
|
||||
args={},
|
||||
tool_context=tool_context,
|
||||
)
|
||||
|
||||
assert res == {
|
||||
"status": "SUCCESS",
|
||||
"_view_parameters": {"user_id": "authenticated-user-999"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_parameterized_view_tool_execution_session_state_fallback():
|
||||
"""Test that BigtableParameterizedViewTool falls back to tool_context.state for custom parameters."""
|
||||
|
||||
def mock_execute_sql(_view_parameters=None):
|
||||
return {"status": "SUCCESS", "_view_parameters": _view_parameters}
|
||||
|
||||
# Create session with application-level state
|
||||
session = Session(
|
||||
id="session-1",
|
||||
app_name="test-app",
|
||||
user_id="user-123",
|
||||
state={"tenant_id": "tenant-xyz"},
|
||||
)
|
||||
|
||||
invocation_context = mock.create_autospec(InvocationContext, instance=True)
|
||||
invocation_context.session = session
|
||||
type(invocation_context).user_id = property(lambda self: self.session.user_id)
|
||||
|
||||
tool_context = Context(invocation_context=invocation_context)
|
||||
|
||||
# Ensure 'tenant_id' is NOT a top-level property or attribute on tool_context
|
||||
assert not hasattr(tool_context, "tenant_id")
|
||||
assert "tenant_id" in tool_context.state
|
||||
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
tool = BigtableParameterizedViewTool(
|
||||
func=mock_execute_sql,
|
||||
view_parameter_names=["tenant_id"],
|
||||
)
|
||||
|
||||
res = await tool._run_async_with_credential(
|
||||
credentials=credentials,
|
||||
tool_settings=BigtableToolSettings(),
|
||||
args={},
|
||||
tool_context=tool_context,
|
||||
)
|
||||
|
||||
assert res == {
|
||||
"status": "SUCCESS",
|
||||
"_view_parameters": {"tenant_id": "tenant-xyz"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bigtable_parameterized_view_tool_execution_multiple_parameters():
|
||||
"""Test that BigtableParameterizedViewTool maps multiple attributes to view_parameters."""
|
||||
|
||||
def mock_execute_sql(_view_parameters=None):
|
||||
return {"status": "SUCCESS", "_view_parameters": _view_parameters}
|
||||
|
||||
session = Session(
|
||||
id="session-1",
|
||||
app_name="test-app",
|
||||
user_id="user-123",
|
||||
state={"tenant_id": "tenant-xyz", "agent_id": "agent-123"},
|
||||
)
|
||||
|
||||
invocation_context = mock.create_autospec(InvocationContext, instance=True)
|
||||
invocation_context.session = session
|
||||
type(invocation_context).user_id = property(lambda self: self.session.user_id)
|
||||
|
||||
tool_context = Context(invocation_context=invocation_context)
|
||||
|
||||
credentials = mock.create_autospec(Credentials, instance=True)
|
||||
# Pass list of parameter names to be matched
|
||||
tool = BigtableParameterizedViewTool(
|
||||
func=mock_execute_sql,
|
||||
view_parameter_names=["user_id", "tenant_id", "agent_id"],
|
||||
)
|
||||
|
||||
res = await tool._run_async_with_credential(
|
||||
credentials=credentials,
|
||||
tool_settings=BigtableToolSettings(),
|
||||
args={},
|
||||
tool_context=tool_context,
|
||||
)
|
||||
|
||||
assert res == {
|
||||
"status": "SUCCESS",
|
||||
"_view_parameters": {
|
||||
"user_id": "user-123",
|
||||
"tenant_id": "tenant-xyz",
|
||||
"agent_id": "agent-123",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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.tools.bigtable import client
|
||||
from google.auth.credentials import Credentials
|
||||
|
||||
|
||||
def test_get_bigtable_data_client():
|
||||
"""Test get_bigtable_client function."""
|
||||
with mock.patch(
|
||||
"google.cloud.bigtable.data.BigtableDataClient"
|
||||
) as MockBigtableDataClient:
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
client.get_bigtable_data_client(
|
||||
project="test-project", credentials=mock_creds
|
||||
)
|
||||
MockBigtableDataClient.assert_called_once_with(
|
||||
project="test-project",
|
||||
credentials=mock_creds,
|
||||
client_info=mock.ANY,
|
||||
)
|
||||
|
||||
|
||||
def test_get_bigtable_admin_client():
|
||||
"""Test get_bigtable_admin_client function."""
|
||||
with mock.patch("google.cloud.bigtable.Client") as BigtableDataClient:
|
||||
mock_creds = mock.create_autospec(Credentials, instance=True)
|
||||
client.get_bigtable_admin_client(
|
||||
project="test-project", credentials=mock_creds
|
||||
)
|
||||
# Admin client is a BigtableDataClient created with admin=True.
|
||||
BigtableDataClient.assert_called_once_with(
|
||||
project="test-project",
|
||||
admin=True,
|
||||
credentials=mock_creds,
|
||||
client_info=mock.ANY,
|
||||
)
|
||||
Reference in New Issue
Block a user