Skip to content

Commit cee1d84

Browse files
committed
Add tests
1 parent 212ecbf commit cee1d84

File tree

2 files changed

+333
-0
lines changed

2 files changed

+333
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import os
2+
import tempfile
3+
from pathlib import Path
4+
import pytest
5+
from unittest.mock import patch, MagicMock
6+
7+
from agno.tools.python import PythonTools
8+
9+
10+
@pytest.fixture
11+
def temp_dir():
12+
with tempfile.TemporaryDirectory() as tmpdirname:
13+
yield Path(tmpdirname)
14+
15+
16+
@pytest.fixture
17+
def python_tools(temp_dir):
18+
return PythonTools(
19+
base_dir=temp_dir,
20+
save_and_run=True,
21+
pip_install=True,
22+
uv_pip_install=True,
23+
run_code=True,
24+
list_files=True,
25+
run_files=True,
26+
read_files=True,
27+
)
28+
29+
30+
def test_save_to_file_and_run_success(python_tools, temp_dir):
31+
# Test successful code execution
32+
code = "x = 42"
33+
result = python_tools.save_to_file_and_run("test.py", code, "x")
34+
assert result == "42"
35+
assert (temp_dir / "test.py").exists()
36+
assert (temp_dir / "test.py").read_text() == code
37+
38+
39+
def test_save_to_file_and_run_error(python_tools):
40+
# Test code with syntax error
41+
code = "x = " # Invalid syntax
42+
result = python_tools.save_to_file_and_run("test.py", code)
43+
assert "Error saving and running code" in result
44+
45+
46+
def test_save_to_file_and_run_no_overwrite(python_tools, temp_dir):
47+
# Test file overwrite prevention
48+
file_path = temp_dir / "test.py"
49+
file_path.write_text("original")
50+
51+
result = python_tools.save_to_file_and_run("test.py", "new code", overwrite=False)
52+
assert "already exists" in result
53+
assert file_path.read_text() == "original"
54+
55+
56+
def test_run_python_file_return_variable(python_tools, temp_dir):
57+
# Test running existing file and returning variable
58+
file_path = temp_dir / "test.py"
59+
file_path.write_text("x = 42")
60+
61+
result = python_tools.run_python_file_return_variable("test.py", "x")
62+
assert result == "42"
63+
64+
65+
def test_run_python_file_return_variable_not_found(python_tools, temp_dir):
66+
# Test running file with non-existent variable
67+
file_path = temp_dir / "test.py"
68+
file_path.write_text("x = 42")
69+
70+
result = python_tools.run_python_file_return_variable("test.py", "y")
71+
assert "Variable y not found" in result
72+
73+
74+
def test_read_file(python_tools, temp_dir):
75+
# Test reading file contents
76+
file_path = temp_dir / "test.txt"
77+
content = "Hello, World!"
78+
file_path.write_text(content)
79+
80+
result = python_tools.read_file("test.txt")
81+
assert result == content
82+
83+
84+
def test_read_file_not_found(python_tools):
85+
# Test reading non-existent file
86+
result = python_tools.read_file("nonexistent.txt")
87+
assert "Error reading file" in result
88+
89+
90+
def test_list_files(python_tools, temp_dir):
91+
# Test listing files in directory
92+
(temp_dir / "file1.txt").touch()
93+
(temp_dir / "file2.txt").touch()
94+
95+
result = python_tools.list_files()
96+
assert "file1.txt" in result
97+
assert "file2.txt" in result
98+
99+
100+
def test_run_python_code(python_tools):
101+
# Test running Python code directly
102+
code = "x = 42"
103+
result = python_tools.run_python_code(code, "x")
104+
assert result == "42"
105+
106+
def test_run_python_code_advanced(python_tools):
107+
# Test running Python code directly
108+
code = """
109+
def fibonacci(n, print_steps: bool = False):
110+
a, b = 0, 1
111+
for _ in range(n):
112+
if print_steps:
113+
print(a)
114+
a, b = b, a + b
115+
return a
116+
117+
result = fibonacci(10, print_steps=True)
118+
"""
119+
result = python_tools.run_python_code(code, "result")
120+
assert result == "55"
121+
122+
def test_run_python_code_error(python_tools):
123+
# Test running invalid Python code
124+
code = "x = " # Invalid syntax
125+
result = python_tools.run_python_code(code)
126+
assert "Error running python code" in result
127+
128+
129+
@patch("subprocess.check_call")
130+
def test_pip_install_package(mock_check_call, python_tools):
131+
# Test pip package installation
132+
result = python_tools.pip_install_package("requests")
133+
assert "successfully installed package requests" in result
134+
mock_check_call.assert_called_once()
135+
136+
137+
@patch("subprocess.check_call")
138+
def test_pip_install_package_error(mock_check_call, python_tools):
139+
# Test pip package installation error
140+
mock_check_call.side_effect = Exception("Installation failed")
141+
result = python_tools.pip_install_package("requests")
142+
assert "Error installing package requests" in result
143+
144+
145+
@patch("subprocess.check_call")
146+
def test_uv_pip_install_package(mock_check_call, python_tools):
147+
# Test uv pip package installation
148+
result = python_tools.uv_pip_install_package("requests")
149+
assert "successfully installed package requests" in result
150+
mock_check_call.assert_called_once()
151+
152+
153+
@patch("subprocess.check_call")
154+
def test_uv_pip_install_package_error(mock_check_call, python_tools):
155+
# Test uv pip package installation error
156+
mock_check_call.side_effect = Exception("Installation failed")
157+
result = python_tools.uv_pip_install_package("requests")
158+
assert "Error installing package requests" in result
159+
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import json
2+
import pytest
3+
from typing import Dict, Optional
4+
5+
from agno.tools.function import Function, FunctionCall
6+
from agno.utils.functions import get_function_call
7+
8+
9+
@pytest.fixture
10+
def sample_functions() -> Dict[str, Function]:
11+
return {
12+
"test_function": Function(
13+
name="test_function",
14+
description="A test function",
15+
parameters={
16+
"type": "object",
17+
"properties": {
18+
"param1": {"type": "string"},
19+
"param2": {"type": "integer"},
20+
"param3": {"type": "boolean"},
21+
},
22+
},
23+
sanitize_arguments=True,
24+
),
25+
"no_sanitize_function": Function(
26+
name="no_sanitize_function",
27+
description="A function without argument sanitization",
28+
parameters={
29+
"type": "object",
30+
"properties": {
31+
"param1": {"type": "string"},
32+
},
33+
},
34+
sanitize_arguments=False,
35+
),
36+
}
37+
38+
39+
def test_get_function_call_basic(sample_functions):
40+
"""Test basic function call creation with valid arguments."""
41+
arguments = json.dumps({"param1": "test", "param2": 42, "param3": True})
42+
call_id = "test-call-123"
43+
44+
result = get_function_call(
45+
name="test_function",
46+
arguments=arguments,
47+
call_id=call_id,
48+
functions=sample_functions,
49+
)
50+
51+
assert result is not None
52+
assert isinstance(result, FunctionCall)
53+
assert result.function == sample_functions["test_function"]
54+
assert result.call_id == call_id
55+
assert result.arguments == {"param1": "test", "param2": 42, "param3": True}
56+
assert result.error is None
57+
58+
59+
def test_get_function_call_invalid_name(sample_functions):
60+
"""Test function call with non-existent function name."""
61+
result = get_function_call(
62+
name="non_existent_function",
63+
arguments='{"param1": "test"}',
64+
functions=sample_functions,
65+
)
66+
67+
assert result is None
68+
69+
70+
def test_get_function_call_no_functions():
71+
"""Test function call with no functions dictionary."""
72+
result = get_function_call(
73+
name="test_function",
74+
arguments='{"param1": "test"}',
75+
functions=None,
76+
)
77+
78+
assert result is None
79+
80+
81+
def test_get_function_call_invalid_json(sample_functions):
82+
"""Test function call with invalid JSON arguments."""
83+
result = get_function_call(
84+
name="test_function",
85+
arguments="invalid json",
86+
functions=sample_functions,
87+
)
88+
89+
assert result is not None
90+
assert result.error is not None
91+
assert "Error while decoding function arguments" in result.error
92+
93+
94+
def test_get_function_call_non_dict_arguments(sample_functions):
95+
"""Test function call with non-dictionary arguments."""
96+
result = get_function_call(
97+
name="test_function",
98+
arguments='["not", "a", "dict"]',
99+
functions=sample_functions,
100+
)
101+
102+
assert result is not None
103+
assert result.error is not None
104+
assert "Function arguments are not a valid JSON object" in result.error
105+
106+
107+
def test_get_function_call_argument_sanitization(sample_functions):
108+
"""Test argument sanitization for boolean and null values."""
109+
arguments = json.dumps({
110+
"param1": "None",
111+
"param2": "True",
112+
"param3": "False",
113+
"param4": " test ",
114+
})
115+
116+
result = get_function_call(
117+
name="test_function",
118+
arguments=arguments,
119+
functions=sample_functions,
120+
)
121+
122+
assert result is not None
123+
assert result.arguments == {
124+
"param1": None,
125+
"param2": True,
126+
"param3": False,
127+
"param4": "test",
128+
}
129+
130+
131+
def test_get_function_call_no_sanitization(sample_functions):
132+
"""Test function call without argument sanitization."""
133+
arguments = json.dumps({
134+
"param1": "None",
135+
"param2": "True",
136+
})
137+
138+
result = get_function_call(
139+
name="no_sanitize_function",
140+
arguments=arguments,
141+
functions=sample_functions,
142+
)
143+
144+
assert result is not None
145+
assert result.arguments == {
146+
"param1": None,
147+
"param2": True,
148+
}
149+
150+
151+
def test_get_function_call_empty_arguments(sample_functions):
152+
"""Test function call with empty arguments."""
153+
result = get_function_call(
154+
name="test_function",
155+
arguments="",
156+
functions=sample_functions,
157+
)
158+
159+
assert result is not None
160+
assert result.arguments is None
161+
assert result.error is None
162+
163+
164+
def test_get_function_call_no_arguments(sample_functions):
165+
"""Test function call with no arguments provided."""
166+
result = get_function_call(
167+
name="test_function",
168+
arguments=None,
169+
functions=sample_functions,
170+
)
171+
172+
assert result is not None
173+
assert result.arguments is None
174+
assert result.error is None

0 commit comments

Comments
 (0)