Skip to content

Commit 079d94c

Browse files
committed
test: Add comprehensive unit tests for Pydantic v2 compatibility module
- Add test_pydantic_v2_compatibility.py with full test coverage - Test successful schema generation and fallback scenarios - Test patch application success and failure cases - Test robust OpenAPI function with various error conditions: * RecursionError handling with recursion limit management * Generic exception handling with fallback schema * AttributeError handling for missing openapi method * Successful schema preservation - Test logging behavior for different error types - Test httpx Client patching functionality - Follow ADK testing patterns with proper Google license header - Use pytest parametrization and mocking best practices - Clean logging without emoji characters for professional output Ensures reliability and maintainability of Pydantic v2 compatibility fixes with comprehensive error handling validation.
1 parent 6ece7d5 commit 079d94c

File tree

1 file changed

+229
-0
lines changed

1 file changed

+229
-0
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.adk.utils.pydantic_v2_compatibility import (
16+
patch_types_for_pydantic_v2,
17+
create_robust_openapi_function,
18+
__get_pydantic_core_schema__,
19+
)
20+
import pytest
21+
from unittest.mock import Mock, patch, MagicMock
22+
from fastapi import FastAPI
23+
import sys
24+
import logging
25+
26+
27+
class TestPydanticV2CompatibilityPatches:
28+
"""Test suite for Pydantic v2 compatibility patches."""
29+
30+
def test_get_pydantic_core_schema_success(self):
31+
"""Test successful schema generation with valid handler."""
32+
mock_handler = Mock()
33+
mock_handler.generate_schema.return_value = {"type": "object", "properties": {}}
34+
35+
result = __get_pydantic_core_schema__(str, mock_handler)
36+
37+
assert result == {"type": "object", "properties": {}}
38+
mock_handler.generate_schema.assert_called_once_with(str)
39+
40+
def test_get_pydantic_core_schema_fallback(self):
41+
"""Test fallback schema when handler fails."""
42+
mock_handler = Mock()
43+
mock_handler.generate_schema.side_effect = Exception("Schema generation failed")
44+
45+
result = __get_pydantic_core_schema__(str, mock_handler)
46+
47+
expected_fallback = {
48+
"type": "object",
49+
"properties": {},
50+
"title": "str",
51+
"_pydantic_v2_compat": True
52+
}
53+
assert result == expected_fallback
54+
55+
def test_get_pydantic_core_schema_no_handler(self):
56+
"""Test schema generation when handler is None."""
57+
result = __get_pydantic_core_schema__(str, None)
58+
59+
expected_fallback = {
60+
"type": "object",
61+
"properties": {},
62+
"title": "str",
63+
"_pydantic_v2_compat": True
64+
}
65+
assert result == expected_fallback
66+
67+
@patch('google.adk.utils.pydantic_v2_compatibility.ClientSession', create=True)
68+
def test_patch_types_for_pydantic_v2_success(self, mock_client_session):
69+
"""Test successful patching of types for Pydantic v2."""
70+
# Mock ClientSession class
71+
mock_client_session.__modify_schema__ = Mock()
72+
73+
result = patch_types_for_pydantic_v2()
74+
75+
assert result is True
76+
# Verify that __get_pydantic_core_schema__ was added
77+
assert hasattr(mock_client_session, '__get_pydantic_core_schema__')
78+
# Verify that __modify_schema__ was removed if it existed
79+
assert not hasattr(mock_client_session, '__modify_schema__')
80+
81+
@patch('google.adk.utils.pydantic_v2_compatibility.ClientSession', side_effect=ImportError)
82+
def test_patch_types_for_pydantic_v2_import_error(self, mock_client_session):
83+
"""Test patching when ClientSession cannot be imported."""
84+
result = patch_types_for_pydantic_v2()
85+
86+
assert result is False
87+
88+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
89+
@patch('google.adk.utils.pydantic_v2_compatibility.ClientSession', create=True)
90+
def test_patch_types_for_pydantic_v2_exception_handling(self, mock_client_session, mock_logger):
91+
"""Test exception handling during patching."""
92+
# Make setattr raise an exception
93+
with patch('builtins.setattr', side_effect=Exception("Patching failed")):
94+
result = patch_types_for_pydantic_v2()
95+
96+
assert result is False
97+
mock_logger.error.assert_called()
98+
99+
def test_create_robust_openapi_function_normal_operation(self):
100+
"""Test robust OpenAPI function under normal conditions."""
101+
mock_app = Mock(spec=FastAPI)
102+
mock_app.openapi.return_value = {"openapi": "3.0.0", "info": {"title": "Test API"}}
103+
104+
robust_openapi = create_robust_openapi_function(mock_app)
105+
result = robust_openapi()
106+
107+
assert result == {"openapi": "3.0.0", "info": {"title": "Test API"}}
108+
109+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
110+
def test_create_robust_openapi_function_recursion_error(self, mock_logger):
111+
"""Test robust OpenAPI function handles RecursionError."""
112+
mock_app = Mock(spec=FastAPI)
113+
mock_app.openapi.side_effect = RecursionError("Maximum recursion depth exceeded")
114+
115+
robust_openapi = create_robust_openapi_function(mock_app)
116+
result = robust_openapi()
117+
118+
# Should return fallback schema
119+
assert "openapi" in result
120+
assert "info" in result
121+
assert result["info"]["title"] == "ADK Agent API"
122+
mock_logger.warning.assert_called()
123+
124+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
125+
@patch('google.adk.utils.pydantic_v2_compatibility.sys')
126+
def test_create_robust_openapi_function_recursion_limit_handling(self, mock_sys, mock_logger):
127+
"""Test recursion limit handling in robust OpenAPI function."""
128+
mock_app = Mock(spec=FastAPI)
129+
mock_app.openapi.return_value = {"openapi": "3.0.0"}
130+
mock_sys.getrecursionlimit.return_value = 1000
131+
132+
robust_openapi = create_robust_openapi_function(mock_app)
133+
result = robust_openapi()
134+
135+
# Verify recursion limit was set
136+
mock_sys.setrecursionlimit.assert_called_with(500)
137+
# Verify it was restored
138+
assert mock_sys.setrecursionlimit.call_count == 2
139+
140+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
141+
def test_create_robust_openapi_function_generic_exception(self, mock_logger):
142+
"""Test robust OpenAPI function handles generic exceptions."""
143+
mock_app = Mock(spec=FastAPI)
144+
mock_app.openapi.side_effect = Exception("Generic error")
145+
146+
robust_openapi = create_robust_openapi_function(mock_app)
147+
result = robust_openapi()
148+
149+
# Should return fallback schema
150+
assert "openapi" in result
151+
assert "info" in result
152+
mock_logger.error.assert_called()
153+
154+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
155+
def test_create_robust_openapi_function_attribute_error(self, mock_logger):
156+
"""Test robust OpenAPI function handles AttributeError."""
157+
mock_app = Mock()
158+
# Remove openapi method to trigger AttributeError
159+
del mock_app.openapi
160+
161+
robust_openapi = create_robust_openapi_function(mock_app)
162+
result = robust_openapi()
163+
164+
# Should return fallback schema
165+
assert "openapi" in result
166+
assert "info" in result
167+
mock_logger.error.assert_called()
168+
169+
def test_robust_openapi_fallback_schema_structure(self):
170+
"""Test the structure of the fallback OpenAPI schema."""
171+
mock_app = Mock(spec=FastAPI)
172+
mock_app.openapi.side_effect = Exception("Error")
173+
174+
robust_openapi = create_robust_openapi_function(mock_app)
175+
result = robust_openapi()
176+
177+
# Verify required OpenAPI structure
178+
assert result["openapi"] == "3.0.0"
179+
assert "info" in result
180+
assert result["info"]["title"] == "ADK Agent API"
181+
assert result["info"]["version"] == "1.0.0"
182+
assert "paths" in result
183+
assert "components" in result
184+
assert "schemas" in result["components"]
185+
186+
@patch('google.adk.utils.pydantic_v2_compatibility.httpx', create=True)
187+
def test_patch_httpx_client_success(self):
188+
"""Test successful patching of httpx Client."""
189+
mock_client = Mock()
190+
191+
with patch('google.adk.utils.pydantic_v2_compatibility.patch_types_for_pydantic_v2') as mock_patch:
192+
mock_patch.return_value = True
193+
result = patch_types_for_pydantic_v2()
194+
195+
assert result is True
196+
197+
def test_robust_openapi_preserves_successful_schema(self):
198+
"""Test that robust OpenAPI preserves successful schema generation."""
199+
mock_app = Mock(spec=FastAPI)
200+
expected_schema = {
201+
"openapi": "3.0.0",
202+
"info": {"title": "Custom API", "version": "2.0.0"},
203+
"paths": {"/test": {"get": {"summary": "Test endpoint"}}},
204+
"components": {"schemas": {"TestModel": {"type": "object"}}}
205+
}
206+
mock_app.openapi.return_value = expected_schema
207+
208+
robust_openapi = create_robust_openapi_function(mock_app)
209+
result = robust_openapi()
210+
211+
assert result == expected_schema
212+
213+
@patch('google.adk.utils.pydantic_v2_compatibility.logger')
214+
def test_create_robust_openapi_logs_errors_appropriately(self, mock_logger):
215+
"""Test that robust OpenAPI function logs errors with appropriate levels."""
216+
mock_app = Mock(spec=FastAPI)
217+
218+
# Test RecursionError logging
219+
mock_app.openapi.side_effect = RecursionError("Recursion error")
220+
robust_openapi = create_robust_openapi_function(mock_app)
221+
robust_openapi()
222+
mock_logger.warning.assert_called()
223+
224+
# Reset and test generic Exception logging
225+
mock_logger.reset_mock()
226+
mock_app.openapi.side_effect = ValueError("Generic error")
227+
robust_openapi = create_robust_openapi_function(mock_app)
228+
robust_openapi()
229+
mock_logger.error.assert_called()

0 commit comments

Comments
 (0)