gemini_adk_rs/code_executors/
mod.rs1pub mod base;
5pub mod built_in;
7pub mod container;
9pub mod types;
11pub mod unsafe_local;
13pub mod utils;
15#[cfg(feature = "vertex-ai-code-executor")]
17pub mod vertex_ai;
18
19pub use base::{CodeExecutor, CodeExecutorError};
20pub use built_in::BuiltInCodeExecutor;
21pub use container::{ContainerCodeExecutor, ContainerCodeExecutorConfig};
22pub use types::{CodeExecutionInput, CodeExecutionResult, CodeFile};
23pub use unsafe_local::UnsafeLocalCodeExecutor;
24#[cfg(feature = "vertex-ai-code-executor")]
25pub use vertex_ai::{VertexAiCodeExecutor, VertexAiCodeExecutorConfig};
26
27#[cfg(test)]
28mod tests {
29 use super::*;
30 use async_trait::async_trait;
31
32 #[test]
35 fn code_file_construction() {
36 let file = CodeFile {
37 name: "main.py".into(),
38 content: "print('hello')".into(),
39 mime_type: "text/x-python".into(),
40 };
41 assert_eq!(file.name, "main.py");
42 assert_eq!(file.content, "print('hello')");
43 assert_eq!(file.mime_type, "text/x-python");
44 }
45
46 #[test]
47 fn code_file_serde_roundtrip() {
48 let file = CodeFile {
49 name: "data.csv".into(),
50 content: "a,b\n1,2".into(),
51 mime_type: "text/csv".into(),
52 };
53 let json = serde_json::to_string(&file).unwrap();
54 let deserialized: CodeFile = serde_json::from_str(&json).unwrap();
55 assert_eq!(deserialized.name, file.name);
56 assert_eq!(deserialized.content, file.content);
57 assert_eq!(deserialized.mime_type, file.mime_type);
58 }
59
60 #[test]
63 fn code_execution_result_empty() {
64 let result = CodeExecutionResult::empty();
65 assert!(result.stdout.is_empty());
66 assert!(result.stderr.is_empty());
67 assert!(result.output_files.is_empty());
68 }
69
70 #[test]
73 fn code_execution_input_construction() {
74 let input = CodeExecutionInput {
75 code: "x = 1 + 2".into(),
76 input_files: vec![CodeFile {
77 name: "input.txt".into(),
78 content: "hello".into(),
79 mime_type: "text/plain".into(),
80 }],
81 execution_id: Some("exec-123".into()),
82 };
83 assert_eq!(input.code, "x = 1 + 2");
84 assert_eq!(input.input_files.len(), 1);
85 assert_eq!(input.execution_id.as_deref(), Some("exec-123"));
86 }
87
88 #[test]
89 fn code_execution_input_no_execution_id() {
90 let input = CodeExecutionInput {
91 code: "pass".into(),
92 input_files: Vec::new(),
93 execution_id: None,
94 };
95 assert!(input.execution_id.is_none());
96 assert!(input.input_files.is_empty());
97 }
98
99 struct StubExecutor;
103
104 #[async_trait]
105 impl CodeExecutor for StubExecutor {
106 async fn execute_code(
107 &self,
108 _input: CodeExecutionInput,
109 ) -> Result<CodeExecutionResult, CodeExecutorError> {
110 Ok(CodeExecutionResult::empty())
111 }
112 }
113
114 #[test]
115 fn default_code_block_delimiters() {
116 let exec = StubExecutor;
117 let delims = exec.code_block_delimiters();
118 assert_eq!(delims.len(), 2);
119 assert_eq!(
120 delims[0],
121 ("```tool_code\n".to_string(), "\n```".to_string())
122 );
123 assert_eq!(delims[1], ("```python\n".to_string(), "\n```".to_string()));
124 }
125
126 #[test]
127 fn default_execution_result_delimiters() {
128 let exec = StubExecutor;
129 let (open, close) = exec.execution_result_delimiters();
130 assert_eq!(open, "```tool_output\n");
131 assert_eq!(close, "\n```");
132 }
133
134 #[test]
135 fn default_error_retry_attempts() {
136 let exec = StubExecutor;
137 assert_eq!(exec.error_retry_attempts(), 2);
138 }
139
140 #[test]
141 fn default_stateful() {
142 let exec = StubExecutor;
143 assert!(!exec.stateful());
144 }
145
146 #[tokio::test]
147 async fn stub_executor_execute_code() {
148 let exec = StubExecutor;
149 let input = CodeExecutionInput {
150 code: "1 + 1".into(),
151 input_files: Vec::new(),
152 execution_id: None,
153 };
154 let result = exec.execute_code(input).await.unwrap();
155 assert!(result.stdout.is_empty());
156 }
157
158 fn _assert_object_safe(_: &dyn CodeExecutor) {}
161
162 #[test]
163 fn code_executor_is_object_safe() {
164 let exec = StubExecutor;
165 _assert_object_safe(&exec);
166 }
167
168 #[test]
171 fn built_in_process_llm_request_adds_code_execution_for_gemini2() {
172 let executor = built_in::BuiltInCodeExecutor;
173 let mut request = crate::llm::LlmRequest::from_text("hello");
174 executor
175 .process_llm_request(&mut request, "gemini-2.5-flash")
176 .unwrap();
177 assert_eq!(request.tools.len(), 1);
178 assert!(request.tools[0].code_execution.is_some());
179 }
180
181 #[test]
182 fn built_in_process_llm_request_rejects_non_gemini2() {
183 let executor = built_in::BuiltInCodeExecutor;
184 let mut request = crate::llm::LlmRequest::from_text("hello");
185 let err = executor
186 .process_llm_request(&mut request, "gemini-1.5-pro")
187 .unwrap_err();
188 assert!(
189 err.to_string().contains("Gemini 2.0+"),
190 "expected UnsupportedModel error, got: {}",
191 err
192 );
193 assert!(request.tools.is_empty());
194 }
195
196 #[tokio::test]
197 async fn built_in_execute_code_returns_empty() {
198 let executor = built_in::BuiltInCodeExecutor;
199 let input = CodeExecutionInput {
200 code: "print('hi')".into(),
201 input_files: Vec::new(),
202 execution_id: None,
203 };
204 let result = executor.execute_code(input).await.unwrap();
205 assert!(result.stdout.is_empty());
206 assert!(result.stderr.is_empty());
207 assert!(result.output_files.is_empty());
208 }
209
210 #[test]
213 fn extract_code_from_tool_code_block() {
214 let text = "Some text\n```tool_code\nprint('hello')\n```\nMore text";
215 let delimiters = vec![
216 ("```tool_code\n".to_string(), "\n```".to_string()),
217 ("```python\n".to_string(), "\n```".to_string()),
218 ];
219 let (code, remaining) = utils::extract_code_from_text(text, &delimiters).unwrap();
220 assert_eq!(code, "print('hello')");
221 assert_eq!(remaining, "Some text\n\nMore text");
222 }
223
224 #[test]
225 fn extract_code_from_python_block() {
226 let text = "Intro\n```python\nx = 42\n```\nDone";
227 let delimiters = vec![
228 ("```tool_code\n".to_string(), "\n```".to_string()),
229 ("```python\n".to_string(), "\n```".to_string()),
230 ];
231 let (code, remaining) = utils::extract_code_from_text(text, &delimiters).unwrap();
232 assert_eq!(code, "x = 42");
233 assert_eq!(remaining, "Intro\n\nDone");
234 }
235
236 #[test]
237 fn extract_code_returns_none_when_no_block() {
238 let text = "Just plain text, no code blocks here.";
239 let delimiters = vec![
240 ("```tool_code\n".to_string(), "\n```".to_string()),
241 ("```python\n".to_string(), "\n```".to_string()),
242 ];
243 assert!(utils::extract_code_from_text(text, &delimiters).is_none());
244 }
245
246 #[test]
247 fn build_executable_code_part_correct() {
248 let part = utils::build_executable_code_part("x = 1");
249 match part {
250 gemini_genai_rs::prelude::Part::ExecutableCode { executable_code } => {
251 assert_eq!(executable_code.language, "PYTHON");
252 assert_eq!(executable_code.code, "x = 1");
253 }
254 other => panic!("expected ExecutableCode, got: {:?}", other),
255 }
256 }
257
258 #[test]
259 fn build_code_execution_result_part_ok_outcome() {
260 let part = utils::build_code_execution_result_part("42\n", "");
261 match part {
262 gemini_genai_rs::prelude::Part::CodeExecutionResult {
263 code_execution_result,
264 } => {
265 assert_eq!(code_execution_result.outcome, "OK");
266 assert_eq!(code_execution_result.output.as_deref(), Some("42\n"));
267 }
268 other => panic!("expected CodeExecutionResult, got: {:?}", other),
269 }
270 }
271
272 #[test]
273 fn build_code_execution_result_part_failed_outcome() {
274 let part =
275 utils::build_code_execution_result_part("partial output", "NameError: x not defined");
276 match part {
277 gemini_genai_rs::prelude::Part::CodeExecutionResult {
278 code_execution_result,
279 } => {
280 assert_eq!(code_execution_result.outcome, "FAILED");
281 let output = code_execution_result.output.unwrap();
282 assert!(output.contains("partial output"));
283 assert!(output.contains("NameError: x not defined"));
284 }
285 other => panic!("expected CodeExecutionResult, got: {:?}", other),
286 }
287 }
288}