From face2e8cf254bb9c96a940dbc743e4926ff5685e Mon Sep 17 00:00:00 2001 From: Amulya Bhatia Date: Thu, 29 May 2025 19:08:23 -0700 Subject: [PATCH] Copybara import of the project: -- 8baeb0b569eaedc638b20e46894178a3b878dbd6 by Amulya Bhatia : test: unit tests for built_in_code_executor and unsafe_code_executor -- cfac73b9271557ead96eb5fb419e05d88c6e8cd4 by Amulya Bhatia : test: unit tests for built_in_code_executor and unsafe_code_executor COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/971 from iamulya:code-executor-tests 55290e27b5e58ef3835905aec88639e936318d01 PiperOrigin-RevId: 764976316 --- .../test_built_in_code_executor.py | 109 ++++++++++++++++++ .../test_unsafe_local_code_executor.py | 87 ++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 tests/unittests/code_executors/test_built_in_code_executor.py create mode 100644 tests/unittests/code_executors/test_unsafe_local_code_executor.py diff --git a/tests/unittests/code_executors/test_built_in_code_executor.py b/tests/unittests/code_executors/test_built_in_code_executor.py new file mode 100644 index 0000000..70f1f54 --- /dev/null +++ b/tests/unittests/code_executors/test_built_in_code_executor.py @@ -0,0 +1,109 @@ +# Copyright 2025 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 pytest +from google.genai import types + +from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor +from google.adk.models.llm_request import LlmRequest + + +@pytest.fixture +def built_in_executor() -> BuiltInCodeExecutor: + return BuiltInCodeExecutor() + + +def test_process_llm_request_gemini_2_model_config_none( + built_in_executor: BuiltInCodeExecutor, +): + """Tests processing when llm_request.config is None for Gemini 2.""" + llm_request = LlmRequest(model="gemini-2.0-flash") + built_in_executor.process_llm_request(llm_request) + assert llm_request.config is not None + assert llm_request.config.tools == [ + types.Tool(code_execution=types.ToolCodeExecution()) + ] + + +def test_process_llm_request_gemini_2_model_tools_none( + built_in_executor: BuiltInCodeExecutor, +): + """Tests processing when llm_request.config.tools is None for Gemini 2.""" + llm_request = LlmRequest( + model="gemini-2.0-pro", config=types.GenerateContentConfig() + ) + built_in_executor.process_llm_request(llm_request) + assert llm_request.config.tools == [ + types.Tool(code_execution=types.ToolCodeExecution()) + ] + + +def test_process_llm_request_gemini_2_model_tools_empty( + built_in_executor: BuiltInCodeExecutor, +): + """Tests processing when llm_request.config.tools is empty for Gemini 2.""" + llm_request = LlmRequest( + model="gemini-2.0-ultra", + config=types.GenerateContentConfig(tools=[]), + ) + built_in_executor.process_llm_request(llm_request) + assert llm_request.config.tools == [ + types.Tool(code_execution=types.ToolCodeExecution()) + ] + + +def test_process_llm_request_gemini_2_model_with_existing_tools( + built_in_executor: BuiltInCodeExecutor, +): + """Tests processing when llm_request.config.tools already has tools for Gemini 2.""" + existing_tool = types.Tool( + function_declarations=[ + types.FunctionDeclaration(name="test_func", description="A test func") + ] + ) + llm_request = LlmRequest( + model="gemini-2.0-flash-001", + config=types.GenerateContentConfig(tools=[existing_tool]), + ) + built_in_executor.process_llm_request(llm_request) + assert len(llm_request.config.tools) == 2 + assert existing_tool in llm_request.config.tools + assert types.Tool( + code_execution=types.ToolCodeExecution() + ) in llm_request.config.tools + + +def test_process_llm_request_non_gemini_2_model( + built_in_executor: BuiltInCodeExecutor, +): + """Tests that a ValueError is raised for non-Gemini 2 models.""" + llm_request = LlmRequest(model="gemini-1.5-flash") + with pytest.raises(ValueError) as excinfo: + built_in_executor.process_llm_request(llm_request) + assert ( + "Gemini code execution tool is not supported for model gemini-1.5-flash" + in str(excinfo.value) + ) + + +def test_process_llm_request_no_model_name( + built_in_executor: BuiltInCodeExecutor, +): + """Tests that a ValueError is raised if model name is not set.""" + llm_request = LlmRequest() # Model name defaults to None + with pytest.raises(ValueError) as excinfo: + built_in_executor.process_llm_request(llm_request) + assert ( + "Gemini code execution tool is not supported for model None" + in str(excinfo.value) + ) \ No newline at end of file diff --git a/tests/unittests/code_executors/test_unsafe_local_code_executor.py b/tests/unittests/code_executors/test_unsafe_local_code_executor.py new file mode 100644 index 0000000..22b31ee --- /dev/null +++ b/tests/unittests/code_executors/test_unsafe_local_code_executor.py @@ -0,0 +1,87 @@ +# Copyright 2025 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 pytest +from unittest.mock import MagicMock +from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor +from google.adk.code_executors.code_execution_utils import CodeExecutionInput, CodeExecutionResult +from google.adk.agents.invocation_context import InvocationContext +from google.adk.agents.base_agent import BaseAgent +from google.adk.sessions.session import Session +from google.adk.sessions.base_session_service import BaseSessionService + + +@pytest.fixture +def mock_invocation_context() -> InvocationContext: + """Provides a mock InvocationContext.""" + mock_agent = MagicMock(spec=BaseAgent) + mock_session = MagicMock(spec=Session) + mock_session_service = MagicMock(spec=BaseSessionService) + return InvocationContext( + invocation_id="test_invocation", + agent=mock_agent, + session=mock_session, + session_service=mock_session_service + ) + + +class TestUnsafeLocalCodeExecutor: + + def test_init_default(self): + executor = UnsafeLocalCodeExecutor() + assert not executor.stateful + assert not executor.optimize_data_file + + def test_init_stateful_raises_error(self): + with pytest.raises(ValueError, match="Cannot set `stateful=True` in UnsafeLocalCodeExecutor."): + UnsafeLocalCodeExecutor(stateful=True) + + def test_init_optimize_data_file_raises_error(self): + with pytest.raises(ValueError, match="Cannot set `optimize_data_file=True` in UnsafeLocalCodeExecutor."): + UnsafeLocalCodeExecutor(optimize_data_file=True) + + def test_execute_code_simple_print(self, mock_invocation_context: InvocationContext): + executor = UnsafeLocalCodeExecutor() + code_input = CodeExecutionInput(code='print("hello world")') + result = executor.execute_code(mock_invocation_context, code_input) + + assert isinstance(result, CodeExecutionResult) + assert result.stdout == "hello world\n" + assert result.stderr == "" + assert result.output_files == [] + + def test_execute_code_with_error(self, mock_invocation_context: InvocationContext): + executor = UnsafeLocalCodeExecutor() + code_input = CodeExecutionInput(code='raise ValueError("Test error")') + result = executor.execute_code(mock_invocation_context, code_input) + + assert isinstance(result, CodeExecutionResult) + assert result.stdout == "" + assert "Test error" in result.stderr + assert result.output_files == [] + + def test_execute_code_variable_assignment(self, mock_invocation_context: InvocationContext): + executor = UnsafeLocalCodeExecutor() + code_input = CodeExecutionInput(code='x = 10\nprint(x * 2)') + result = executor.execute_code(mock_invocation_context, code_input) + + assert result.stdout == "20\n" + assert result.stderr == "" + + def test_execute_code_empty(self, mock_invocation_context: InvocationContext): + executor = UnsafeLocalCodeExecutor() + code_input = CodeExecutionInput(code='') + result = executor.execute_code(mock_invocation_context, code_input) + assert result.stdout == "" + assert result.stderr == "" \ No newline at end of file