Skip to content

Commit 7cbc4c8

Browse files
committed
added more test to verify the changes
Signed-off-by: Nikhil Suri <[email protected]>
1 parent 4cb87b1 commit 7cbc4c8

File tree

2 files changed

+429
-0
lines changed

2 files changed

+429
-0
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""
2+
Unit tests specifically for telemetry_push_client RequestError handling
3+
with http-code context extraction for rate limiting detection.
4+
"""
5+
6+
import pytest
7+
from unittest.mock import Mock, patch
8+
9+
from databricks.sql.telemetry.telemetry_push_client import (
10+
CircuitBreakerTelemetryPushClient,
11+
TelemetryPushClient,
12+
)
13+
from databricks.sql.common.http import HttpMethod
14+
from databricks.sql.exc import RequestError, TelemetryRateLimitError
15+
from databricks.sql.telemetry.circuit_breaker_manager import (
16+
CircuitBreakerManager,
17+
CircuitBreakerConfig,
18+
)
19+
20+
21+
class TestTelemetryPushClientRequestErrorHandling:
22+
"""Test RequestError handling and http-code context extraction."""
23+
24+
@pytest.fixture
25+
def setup_circuit_breaker(self):
26+
"""Setup circuit breaker for testing."""
27+
CircuitBreakerManager._instances.clear()
28+
CircuitBreakerManager.initialize(CircuitBreakerConfig())
29+
yield
30+
CircuitBreakerManager._instances.clear()
31+
CircuitBreakerManager._config = None
32+
33+
@pytest.fixture
34+
def mock_delegate(self):
35+
"""Create mock delegate client."""
36+
return Mock(spec=TelemetryPushClient)
37+
38+
@pytest.fixture
39+
def client(self, mock_delegate, setup_circuit_breaker):
40+
"""Create CircuitBreakerTelemetryPushClient instance."""
41+
return CircuitBreakerTelemetryPushClient(
42+
mock_delegate, "test-host.example.com"
43+
)
44+
45+
def test_request_error_with_http_code_429_triggers_rate_limit_error(
46+
self, client, mock_delegate
47+
):
48+
"""Test that RequestError with http-code=429 raises TelemetryRateLimitError."""
49+
# Create RequestError with http-code in context
50+
request_error = RequestError(
51+
"HTTP request failed", context={"http-code": 429}
52+
)
53+
mock_delegate.request.side_effect = request_error
54+
55+
# Should return mock success (circuit breaker handles TelemetryRateLimitError)
56+
response = client.request(HttpMethod.POST, "https://test.com", {})
57+
assert response is not None
58+
assert response.status == 200 # Mock success
59+
60+
def test_request_error_with_http_code_503_triggers_rate_limit_error(
61+
self, client, mock_delegate
62+
):
63+
"""Test that RequestError with http-code=503 raises TelemetryRateLimitError."""
64+
request_error = RequestError(
65+
"HTTP request failed", context={"http-code": 503}
66+
)
67+
mock_delegate.request.side_effect = request_error
68+
69+
# Should return mock success
70+
response = client.request(HttpMethod.POST, "https://test.com", {})
71+
assert response is not None
72+
assert response.status == 200
73+
74+
def test_request_error_with_http_code_500_returns_mock_success(
75+
self, client, mock_delegate
76+
):
77+
"""Test that RequestError with http-code=500 does NOT trigger rate limit error."""
78+
request_error = RequestError(
79+
"HTTP request failed", context={"http-code": 500}
80+
)
81+
mock_delegate.request.side_effect = request_error
82+
83+
# Should return mock success (500 is NOT rate limiting)
84+
response = client.request(HttpMethod.POST, "https://test.com", {})
85+
assert response is not None
86+
assert response.status == 200
87+
88+
def test_request_error_without_http_code_returns_mock_success(
89+
self, client, mock_delegate
90+
):
91+
"""Test that RequestError without http-code context returns mock success."""
92+
# RequestError with empty context
93+
request_error = RequestError("HTTP request failed", context={})
94+
mock_delegate.request.side_effect = request_error
95+
96+
# Should return mock success (no rate limiting)
97+
response = client.request(HttpMethod.POST, "https://test.com", {})
98+
assert response is not None
99+
assert response.status == 200
100+
101+
def test_request_error_with_none_context_returns_mock_success(
102+
self, client, mock_delegate
103+
):
104+
"""Test that RequestError with None context does not crash."""
105+
# RequestError with no context attribute
106+
request_error = RequestError("HTTP request failed")
107+
request_error.context = None
108+
mock_delegate.request.side_effect = request_error
109+
110+
# Should return mock success (no crash)
111+
response = client.request(HttpMethod.POST, "https://test.com", {})
112+
assert response is not None
113+
assert response.status == 200
114+
115+
def test_request_error_missing_context_attribute(self, client, mock_delegate):
116+
"""Test RequestError without context attribute does not crash."""
117+
request_error = RequestError("HTTP request failed")
118+
# Ensure no context attribute exists
119+
if hasattr(request_error, "context"):
120+
delattr(request_error, "context")
121+
mock_delegate.request.side_effect = request_error
122+
123+
# Should return mock success (no crash checking hasattr)
124+
response = client.request(HttpMethod.POST, "https://test.com", {})
125+
assert response is not None
126+
assert response.status == 200
127+
128+
def test_request_error_with_http_code_429_logs_warning(
129+
self, client, mock_delegate
130+
):
131+
"""Test that rate limit errors log at warning level."""
132+
with patch("databricks.sql.telemetry.telemetry_push_client.logger") as mock_logger:
133+
request_error = RequestError(
134+
"HTTP request failed", context={"http-code": 429}
135+
)
136+
mock_delegate.request.side_effect = request_error
137+
138+
client.request(HttpMethod.POST, "https://test.com", {})
139+
140+
# Should log warning for rate limiting
141+
mock_logger.warning.assert_called()
142+
warning_args = mock_logger.warning.call_args[0]
143+
assert "429" in str(warning_args)
144+
assert "circuit breaker" in warning_args[0].lower()
145+
146+
def test_request_error_with_http_code_500_logs_debug(
147+
self, client, mock_delegate
148+
):
149+
"""Test that non-rate-limit errors log at debug level."""
150+
with patch("databricks.sql.telemetry.telemetry_push_client.logger") as mock_logger:
151+
request_error = RequestError(
152+
"HTTP request failed", context={"http-code": 500}
153+
)
154+
mock_delegate.request.side_effect = request_error
155+
156+
client.request(HttpMethod.POST, "https://test.com", {})
157+
158+
# Should log debug for non-rate-limit errors
159+
mock_logger.debug.assert_called()
160+
debug_args = mock_logger.debug.call_args[0]
161+
assert "failing silently" in debug_args[0].lower()
162+
163+
def test_request_error_with_string_http_code(self, client, mock_delegate):
164+
"""Test RequestError with http-code as string (edge case)."""
165+
# Edge case: http-code as string instead of int
166+
request_error = RequestError(
167+
"HTTP request failed", context={"http-code": "429"}
168+
)
169+
mock_delegate.request.side_effect = request_error
170+
171+
# Should handle gracefully (string "429" not in [429, 503])
172+
response = client.request(HttpMethod.POST, "https://test.com", {})
173+
assert response is not None
174+
assert response.status == 200
175+
176+
def test_http_code_extraction_prioritization(self, client, mock_delegate):
177+
"""Test that http-code from RequestError context is correctly extracted."""
178+
# This test verifies the exact code path in telemetry_push_client
179+
request_error = RequestError(
180+
"HTTP request failed after retries", context={"http-code": 503}
181+
)
182+
mock_delegate.request.side_effect = request_error
183+
184+
with patch("databricks.sql.telemetry.telemetry_push_client.logger") as mock_logger:
185+
response = client.request(HttpMethod.POST, "https://test.com", {})
186+
187+
# Verify warning logged with correct status code
188+
mock_logger.warning.assert_called()
189+
warning_call = mock_logger.warning.call_args[0]
190+
assert "503" in str(warning_call)
191+
assert "retries exhausted" in warning_call[0].lower()
192+
193+
# Verify mock success returned
194+
assert response.status == 200
195+
196+
def test_non_request_error_exceptions_handled(self, client, mock_delegate):
197+
"""Test that non-RequestError exceptions are handled gracefully."""
198+
# Generic exception (not RequestError)
199+
generic_error = ValueError("Network timeout")
200+
mock_delegate.request.side_effect = generic_error
201+
202+
# Should return mock success (non-RequestError handled)
203+
response = client.request(HttpMethod.POST, "https://test.com", {})
204+
assert response is not None
205+
assert response.status == 200
206+

0 commit comments

Comments
 (0)