hud-python 0.4.52__py3-none-any.whl → 0.4.53__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/agents/base.py +9 -2
- hud/agents/openai_chat_generic.py +15 -3
- hud/agents/tests/test_base.py +15 -0
- hud/agents/tests/test_base_runtime.py +164 -0
- hud/cli/__init__.py +6 -3
- hud/cli/build.py +35 -27
- hud/cli/dev.py +11 -29
- hud/cli/eval.py +61 -61
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +24 -2
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +134 -0
- hud/cli/tests/test_mcp_server.py +8 -7
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/utils/docker.py +120 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +2 -2
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_runner.py +106 -0
- hud/datasets/tests/test_utils.py +228 -0
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_instrumentation.py +207 -0
- hud/server/tests/test_server_extra.py +2 -0
- hud/shared/exceptions.py +35 -4
- hud/shared/hints.py +25 -0
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +31 -23
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/tests/test_async_context.py +242 -0
- hud/telemetry/tests/test_instrument.py +414 -0
- hud/telemetry/tests/test_job.py +609 -0
- hud/telemetry/tests/test_trace.py +183 -5
- hud/tools/computer/settings.py +2 -2
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/types.py +7 -1
- hud/utils/agent_factories.py +1 -3
- hud/utils/mcp.py +1 -1
- hud/utils/tests/test_agent_factories.py +60 -0
- hud/utils/tests/test_mcp.py +4 -6
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tasks.py +187 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/METADATA +47 -48
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/RECORD +69 -31
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/WHEEL +0 -0
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.telemetry.async_context import async_job, async_trace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_async_trace_basic():
|
|
12
|
+
"""Test basic AsyncTrace usage."""
|
|
13
|
+
with (
|
|
14
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
15
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
16
|
+
patch("hud.telemetry.async_context._print_trace_url"),
|
|
17
|
+
patch("hud.telemetry.async_context._print_trace_complete_url"),
|
|
18
|
+
):
|
|
19
|
+
mock_otel_instance = MagicMock()
|
|
20
|
+
mock_otel.return_value = mock_otel_instance
|
|
21
|
+
|
|
22
|
+
async with async_trace("Test Task") as trace_obj:
|
|
23
|
+
assert trace_obj.name == "Test Task"
|
|
24
|
+
assert trace_obj.id is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_async_trace_with_job_id():
|
|
29
|
+
"""Test AsyncTrace with job_id parameter."""
|
|
30
|
+
with (
|
|
31
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
32
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
33
|
+
):
|
|
34
|
+
mock_otel_instance = MagicMock()
|
|
35
|
+
mock_otel.return_value = mock_otel_instance
|
|
36
|
+
|
|
37
|
+
async with async_trace("Test", job_id="job-123") as trace_obj:
|
|
38
|
+
assert trace_obj.job_id == "job-123"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_async_trace_with_task_id():
|
|
43
|
+
"""Test AsyncTrace with task_id parameter."""
|
|
44
|
+
with (
|
|
45
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
46
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
47
|
+
):
|
|
48
|
+
mock_otel_instance = MagicMock()
|
|
49
|
+
mock_otel.return_value = mock_otel_instance
|
|
50
|
+
|
|
51
|
+
async with async_trace("Test", task_id="task-456") as trace_obj:
|
|
52
|
+
assert trace_obj.task_id == "task-456"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_async_trace_prints_url_without_job():
|
|
57
|
+
"""Test AsyncTrace prints URL when not part of a job."""
|
|
58
|
+
with (
|
|
59
|
+
patch("hud.telemetry.async_context.settings") as mock_settings,
|
|
60
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
61
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
62
|
+
patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
|
|
63
|
+
):
|
|
64
|
+
mock_settings.telemetry_enabled = True
|
|
65
|
+
mock_settings.api_key = "test-key"
|
|
66
|
+
mock_otel_instance = MagicMock()
|
|
67
|
+
mock_otel.return_value = mock_otel_instance
|
|
68
|
+
|
|
69
|
+
async with async_trace("Test", job_id=None):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Should print trace URL
|
|
73
|
+
mock_print_url.assert_called_once()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_async_trace_no_print_url_with_job():
|
|
78
|
+
"""Test AsyncTrace doesn't print URL when part of a job."""
|
|
79
|
+
with (
|
|
80
|
+
patch("hud.telemetry.async_context.settings") as mock_settings,
|
|
81
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
82
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
83
|
+
patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
|
|
84
|
+
):
|
|
85
|
+
mock_settings.telemetry_enabled = True
|
|
86
|
+
mock_settings.api_key = "test-key"
|
|
87
|
+
mock_otel_instance = MagicMock()
|
|
88
|
+
mock_otel.return_value = mock_otel_instance
|
|
89
|
+
|
|
90
|
+
async with async_trace("Test", job_id="job-123"):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
# Should NOT print trace URL when job_id is set
|
|
94
|
+
mock_print_url.assert_not_called()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_async_trace_with_exception():
|
|
99
|
+
"""Test AsyncTrace handles exceptions."""
|
|
100
|
+
with (
|
|
101
|
+
patch("hud.telemetry.async_context.settings") as mock_settings,
|
|
102
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
103
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
104
|
+
patch("hud.telemetry.async_context._print_trace_complete_url") as mock_print,
|
|
105
|
+
):
|
|
106
|
+
# Enable telemetry for this test
|
|
107
|
+
mock_settings.telemetry_enabled = True
|
|
108
|
+
mock_settings.api_key = "test-key"
|
|
109
|
+
|
|
110
|
+
mock_otel_instance = MagicMock()
|
|
111
|
+
mock_otel.return_value = mock_otel_instance
|
|
112
|
+
|
|
113
|
+
with pytest.raises(ValueError):
|
|
114
|
+
async with async_trace("Test"):
|
|
115
|
+
raise ValueError("Test error")
|
|
116
|
+
|
|
117
|
+
# Should have been called with error_occurred keyword arg
|
|
118
|
+
mock_print.assert_called_once()
|
|
119
|
+
call_kwargs = mock_print.call_args[1]
|
|
120
|
+
assert call_kwargs["error_occurred"] is True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_async_job_basic():
|
|
125
|
+
"""Test basic AsyncJob usage."""
|
|
126
|
+
with (
|
|
127
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
128
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
129
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
130
|
+
):
|
|
131
|
+
async with async_job("Test Job") as job_obj:
|
|
132
|
+
assert job_obj.name == "Test Job"
|
|
133
|
+
assert job_obj.id is not None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_async_job_with_metadata():
|
|
138
|
+
"""Test AsyncJob with metadata."""
|
|
139
|
+
with (
|
|
140
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
141
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
142
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
143
|
+
):
|
|
144
|
+
async with async_job("Test", metadata={"key": "value"}) as job_obj:
|
|
145
|
+
assert job_obj.metadata == {"key": "value"}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_async_job_with_dataset_link():
|
|
150
|
+
"""Test AsyncJob with dataset_link."""
|
|
151
|
+
with (
|
|
152
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
153
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
154
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
155
|
+
):
|
|
156
|
+
async with async_job("Test", dataset_link="test/dataset") as job_obj:
|
|
157
|
+
assert job_obj.dataset_link == "test/dataset"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_async_job_with_custom_job_id():
|
|
162
|
+
"""Test AsyncJob with custom job_id."""
|
|
163
|
+
with (
|
|
164
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
165
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
166
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
167
|
+
):
|
|
168
|
+
async with async_job("Test", job_id="custom-id") as job_obj:
|
|
169
|
+
assert job_obj.id == "custom-id"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.asyncio
|
|
173
|
+
async def test_async_job_with_exception():
|
|
174
|
+
"""Test AsyncJob handles exceptions."""
|
|
175
|
+
with (
|
|
176
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
177
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
178
|
+
patch("hud.telemetry.async_context._print_job_complete_url") as mock_print,
|
|
179
|
+
):
|
|
180
|
+
with pytest.raises(ValueError):
|
|
181
|
+
async with async_job("Test"):
|
|
182
|
+
raise ValueError("Job error")
|
|
183
|
+
|
|
184
|
+
# Should print with error_occurred keyword arg
|
|
185
|
+
mock_print.assert_called_once()
|
|
186
|
+
call_kwargs = mock_print.call_args[1]
|
|
187
|
+
assert call_kwargs["error_occurred"] is True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_async_job_status_updates():
|
|
192
|
+
"""Test AsyncJob sends status updates."""
|
|
193
|
+
with (
|
|
194
|
+
patch("hud.telemetry.async_context.settings") as mock_settings,
|
|
195
|
+
patch("hud.telemetry.async_context.track_task") as mock_track,
|
|
196
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
197
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
198
|
+
):
|
|
199
|
+
mock_settings.telemetry_enabled = True
|
|
200
|
+
mock_settings.api_key = "test-key"
|
|
201
|
+
mock_settings.hud_telemetry_url = "https://test.com"
|
|
202
|
+
|
|
203
|
+
async with async_job("Test"):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
# Should have called track_task twice (running and completed)
|
|
207
|
+
assert mock_track.call_count == 2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_async_job_includes_dataset_link_in_status():
|
|
212
|
+
"""Test AsyncJob includes dataset_link in status updates."""
|
|
213
|
+
with (
|
|
214
|
+
patch("hud.telemetry.async_context.settings") as mock_settings,
|
|
215
|
+
patch("hud.telemetry.async_context.track_task"),
|
|
216
|
+
patch("hud.telemetry.async_context.make_request", new_callable=AsyncMock),
|
|
217
|
+
patch("hud.telemetry.async_context._print_job_url"),
|
|
218
|
+
patch("hud.telemetry.async_context._print_job_complete_url"),
|
|
219
|
+
):
|
|
220
|
+
mock_settings.telemetry_enabled = True
|
|
221
|
+
mock_settings.api_key = "test-key"
|
|
222
|
+
mock_settings.hud_telemetry_url = "https://test.com"
|
|
223
|
+
|
|
224
|
+
async with async_job("Test", dataset_link="test/dataset"):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
async def test_async_trace_non_root():
|
|
230
|
+
"""Test AsyncTrace with root=False."""
|
|
231
|
+
with (
|
|
232
|
+
patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
|
|
233
|
+
patch("hud.telemetry.async_context.track_task") as mock_track,
|
|
234
|
+
):
|
|
235
|
+
mock_otel_instance = MagicMock()
|
|
236
|
+
mock_otel.return_value = mock_otel_instance
|
|
237
|
+
|
|
238
|
+
async with async_trace("Test", root=False):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
# Should not track status updates for non-root traces
|
|
242
|
+
mock_track.assert_not_called()
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from opentelemetry.trace import SpanKind
|
|
7
|
+
|
|
8
|
+
from hud.telemetry.instrument import _serialize_value, instrument
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_serialize_value_simple_types():
|
|
12
|
+
"""Test _serialize_value with simple types."""
|
|
13
|
+
assert _serialize_value("string") == "string"
|
|
14
|
+
assert _serialize_value(42) == 42
|
|
15
|
+
assert _serialize_value(3.14) == 3.14
|
|
16
|
+
assert _serialize_value(True) is True
|
|
17
|
+
assert _serialize_value(None) is None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_serialize_value_list():
|
|
21
|
+
"""Test _serialize_value with lists."""
|
|
22
|
+
result = _serialize_value([1, 2, 3])
|
|
23
|
+
assert result == [1, 2, 3]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_serialize_value_list_truncation():
|
|
27
|
+
"""Test _serialize_value truncates long lists."""
|
|
28
|
+
long_list = list(range(20))
|
|
29
|
+
result = _serialize_value(long_list, max_items=5)
|
|
30
|
+
assert len(result) == 5
|
|
31
|
+
assert result == [0, 1, 2, 3, 4]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_serialize_value_tuple():
|
|
35
|
+
"""Test _serialize_value with tuples."""
|
|
36
|
+
result = _serialize_value((1, 2, 3))
|
|
37
|
+
assert result == [1, 2, 3] # Converted to list by JSON
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_serialize_value_tuple_truncation():
|
|
41
|
+
"""Test _serialize_value truncates long tuples."""
|
|
42
|
+
long_tuple = tuple(range(20))
|
|
43
|
+
result = _serialize_value(long_tuple, max_items=5)
|
|
44
|
+
assert len(result) == 5
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_serialize_value_dict():
|
|
48
|
+
"""Test _serialize_value with dicts."""
|
|
49
|
+
result = _serialize_value({"key": "value"})
|
|
50
|
+
assert result == {"key": "value"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_serialize_value_dict_truncation():
|
|
54
|
+
"""Test _serialize_value truncates large dicts."""
|
|
55
|
+
large_dict = {f"key{i}": i for i in range(20)}
|
|
56
|
+
result = _serialize_value(large_dict, max_items=5)
|
|
57
|
+
assert len(result) == 5
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_serialize_value_complex_object():
|
|
61
|
+
"""Test _serialize_value with custom objects."""
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class CustomObj:
|
|
65
|
+
name: str
|
|
66
|
+
value: int
|
|
67
|
+
|
|
68
|
+
obj = CustomObj(name="test", value=42)
|
|
69
|
+
result = _serialize_value(obj)
|
|
70
|
+
assert isinstance(result, dict)
|
|
71
|
+
assert result["name"] == "test"
|
|
72
|
+
assert result["value"] == 42
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_serialize_value_fallback():
|
|
76
|
+
"""Test _serialize_value fallback for non-serializable objects."""
|
|
77
|
+
|
|
78
|
+
class WeirdObj:
|
|
79
|
+
def __init__(self):
|
|
80
|
+
raise Exception("Can't access")
|
|
81
|
+
|
|
82
|
+
obj = WeirdObj.__new__(WeirdObj)
|
|
83
|
+
result = _serialize_value(obj)
|
|
84
|
+
# The result is a string representation of the object
|
|
85
|
+
assert isinstance(result, str)
|
|
86
|
+
assert "WeirdObj" in result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_instrument_async_basic():
|
|
91
|
+
"""Test instrument decorator on async function."""
|
|
92
|
+
|
|
93
|
+
@instrument
|
|
94
|
+
async def test_func(x: int, y: int) -> int:
|
|
95
|
+
return x + y
|
|
96
|
+
|
|
97
|
+
result = await test_func(2, 3)
|
|
98
|
+
assert result == 5
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
async def test_instrument_async_with_params():
|
|
103
|
+
"""Test instrument with custom parameters."""
|
|
104
|
+
|
|
105
|
+
@instrument(name="custom_name", span_type="custom_type")
|
|
106
|
+
async def test_func(x: int) -> int:
|
|
107
|
+
return x * 2
|
|
108
|
+
|
|
109
|
+
result = await test_func(5)
|
|
110
|
+
assert result == 10
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_instrument_async_with_exception():
|
|
115
|
+
"""Test instrument handles exceptions."""
|
|
116
|
+
|
|
117
|
+
@instrument
|
|
118
|
+
async def test_func():
|
|
119
|
+
raise ValueError("Test error")
|
|
120
|
+
|
|
121
|
+
with pytest.raises(ValueError, match="Test error"):
|
|
122
|
+
await test_func()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_instrument_async_no_record_args():
|
|
127
|
+
"""Test instrument with record_args=False."""
|
|
128
|
+
|
|
129
|
+
@instrument(record_args=False)
|
|
130
|
+
async def test_func(x: int) -> int:
|
|
131
|
+
return x
|
|
132
|
+
|
|
133
|
+
result = await test_func(42)
|
|
134
|
+
assert result == 42
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_instrument_async_no_record_result():
|
|
139
|
+
"""Test instrument with record_result=False."""
|
|
140
|
+
|
|
141
|
+
@instrument(record_result=False)
|
|
142
|
+
async def test_func() -> str:
|
|
143
|
+
return "test"
|
|
144
|
+
|
|
145
|
+
result = await test_func()
|
|
146
|
+
assert result == "test"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_instrument_async_with_attributes():
|
|
151
|
+
"""Test instrument with custom attributes."""
|
|
152
|
+
|
|
153
|
+
@instrument(attributes={"custom_attr": "value"})
|
|
154
|
+
async def test_func() -> int:
|
|
155
|
+
return 42
|
|
156
|
+
|
|
157
|
+
result = await test_func()
|
|
158
|
+
assert result == 42
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.asyncio
|
|
162
|
+
async def test_instrument_async_with_span_kind():
|
|
163
|
+
"""Test instrument with custom span kind."""
|
|
164
|
+
|
|
165
|
+
@instrument(span_kind=SpanKind.CLIENT)
|
|
166
|
+
async def test_func() -> int:
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
result = await test_func()
|
|
170
|
+
assert result == 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_instrument_sync_basic():
|
|
174
|
+
"""Test instrument decorator on sync function."""
|
|
175
|
+
|
|
176
|
+
@instrument
|
|
177
|
+
def test_func(x: int, y: int) -> int:
|
|
178
|
+
return x + y
|
|
179
|
+
|
|
180
|
+
result = test_func(2, 3)
|
|
181
|
+
assert result == 5
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_instrument_sync_with_params():
|
|
185
|
+
"""Test instrument on sync function with parameters."""
|
|
186
|
+
|
|
187
|
+
@instrument(name="sync_custom", span_type="sync_type")
|
|
188
|
+
def test_func(x: int) -> int:
|
|
189
|
+
return x * 2
|
|
190
|
+
|
|
191
|
+
result = test_func(5)
|
|
192
|
+
assert result == 10
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_instrument_sync_with_exception():
|
|
196
|
+
"""Test instrument handles exceptions in sync functions."""
|
|
197
|
+
|
|
198
|
+
@instrument
|
|
199
|
+
def test_func():
|
|
200
|
+
raise ValueError("Sync error")
|
|
201
|
+
|
|
202
|
+
with pytest.raises(ValueError, match="Sync error"):
|
|
203
|
+
test_func()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_instrument_sync_no_record_args():
|
|
207
|
+
"""Test instrument sync with record_args=False."""
|
|
208
|
+
|
|
209
|
+
@instrument(record_args=False)
|
|
210
|
+
def test_func(x: int) -> int:
|
|
211
|
+
return x
|
|
212
|
+
|
|
213
|
+
result = test_func(42)
|
|
214
|
+
assert result == 42
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_instrument_sync_no_record_result():
|
|
218
|
+
"""Test instrument sync with record_result=False."""
|
|
219
|
+
|
|
220
|
+
@instrument(record_result=False)
|
|
221
|
+
def test_func() -> str:
|
|
222
|
+
return "test"
|
|
223
|
+
|
|
224
|
+
result = test_func()
|
|
225
|
+
assert result == "test"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_instrument_sync_with_attributes():
|
|
229
|
+
"""Test instrument sync with custom attributes."""
|
|
230
|
+
|
|
231
|
+
@instrument(attributes={"sync_attr": "sync_value"})
|
|
232
|
+
def test_func() -> int:
|
|
233
|
+
return 42
|
|
234
|
+
|
|
235
|
+
result = test_func()
|
|
236
|
+
assert result == 42
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_instrument_already_instrumented():
|
|
240
|
+
"""Test that instrumenting already instrumented function is skipped."""
|
|
241
|
+
|
|
242
|
+
@instrument
|
|
243
|
+
def test_func():
|
|
244
|
+
return "original"
|
|
245
|
+
|
|
246
|
+
# Try to instrument again
|
|
247
|
+
test_func2 = instrument(test_func)
|
|
248
|
+
|
|
249
|
+
# Should be the same function
|
|
250
|
+
assert test_func2 is test_func
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_instrument_marks_as_instrumented():
|
|
254
|
+
"""Test that instrument marks functions correctly."""
|
|
255
|
+
|
|
256
|
+
@instrument
|
|
257
|
+
def test_func():
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
assert hasattr(test_func, "_hud_instrumented")
|
|
261
|
+
assert test_func._hud_instrumented is True
|
|
262
|
+
assert hasattr(test_func, "_hud_original")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@pytest.mark.asyncio
|
|
266
|
+
async def test_instrument_async_complex_result():
|
|
267
|
+
"""Test instrument with complex result object."""
|
|
268
|
+
|
|
269
|
+
@instrument
|
|
270
|
+
async def test_func() -> dict:
|
|
271
|
+
return {"nested": {"data": [1, 2, 3]}, "count": 3}
|
|
272
|
+
|
|
273
|
+
result = await test_func()
|
|
274
|
+
assert result["count"] == 3
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_instrument_sync_complex_result():
|
|
278
|
+
"""Test instrument sync with complex result."""
|
|
279
|
+
|
|
280
|
+
@dataclass
|
|
281
|
+
class Result:
|
|
282
|
+
value: int
|
|
283
|
+
name: str
|
|
284
|
+
|
|
285
|
+
@instrument
|
|
286
|
+
def test_func() -> Result:
|
|
287
|
+
return Result(value=42, name="test")
|
|
288
|
+
|
|
289
|
+
result = test_func()
|
|
290
|
+
assert result.value == 42
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.mark.asyncio
|
|
294
|
+
async def test_instrument_async_with_self_param():
|
|
295
|
+
"""Test instrument properly handles 'self' parameter."""
|
|
296
|
+
|
|
297
|
+
class TestClass:
|
|
298
|
+
@instrument
|
|
299
|
+
async def method(self, x: int) -> int:
|
|
300
|
+
return x * 2
|
|
301
|
+
|
|
302
|
+
obj = TestClass()
|
|
303
|
+
result = await obj.method(5)
|
|
304
|
+
assert result == 10
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_instrument_sync_with_cls_param():
|
|
308
|
+
"""Test instrument properly handles 'cls' parameter."""
|
|
309
|
+
|
|
310
|
+
class TestClass:
|
|
311
|
+
@classmethod
|
|
312
|
+
@instrument
|
|
313
|
+
def method(cls, x: int) -> int:
|
|
314
|
+
return x * 3
|
|
315
|
+
|
|
316
|
+
result = TestClass.method(4)
|
|
317
|
+
assert result == 12
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_instrument_async_serialization_error():
|
|
322
|
+
"""Test instrument handles serialization errors gracefully."""
|
|
323
|
+
|
|
324
|
+
class UnserializableArg:
|
|
325
|
+
def __getattribute__(self, name):
|
|
326
|
+
raise Exception("Can't serialize")
|
|
327
|
+
|
|
328
|
+
@instrument
|
|
329
|
+
async def test_func(arg):
|
|
330
|
+
return "success"
|
|
331
|
+
|
|
332
|
+
# Should not raise, just skip serialization
|
|
333
|
+
result = await test_func(UnserializableArg())
|
|
334
|
+
assert result == "success"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_instrument_function_without_signature():
|
|
338
|
+
"""Test instrument on functions without inspectable signature."""
|
|
339
|
+
# Built-in functions don't have signatures
|
|
340
|
+
instrumented_len = instrument(len)
|
|
341
|
+
result = instrumented_len([1, 2, 3])
|
|
342
|
+
assert result == 3
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.mark.asyncio
|
|
346
|
+
async def test_instrument_async_result_serialization_error():
|
|
347
|
+
"""Test instrument handles result serialization errors."""
|
|
348
|
+
|
|
349
|
+
class UnserializableResult:
|
|
350
|
+
def __iter__(self):
|
|
351
|
+
raise Exception("Can't iterate")
|
|
352
|
+
|
|
353
|
+
@instrument
|
|
354
|
+
async def test_func():
|
|
355
|
+
return UnserializableResult()
|
|
356
|
+
|
|
357
|
+
# Should not raise, just skip result recording
|
|
358
|
+
result = await test_func()
|
|
359
|
+
assert isinstance(result, UnserializableResult)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_instrument_without_parentheses():
|
|
363
|
+
"""Test using @instrument without parentheses."""
|
|
364
|
+
|
|
365
|
+
@instrument
|
|
366
|
+
def test_func(x: int) -> int:
|
|
367
|
+
return x + 1
|
|
368
|
+
|
|
369
|
+
assert test_func(5) == 6
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_instrument_with_parentheses():
|
|
373
|
+
"""Test using @instrument() with parentheses."""
|
|
374
|
+
|
|
375
|
+
@instrument()
|
|
376
|
+
def test_func(x: int) -> int:
|
|
377
|
+
return x + 1
|
|
378
|
+
|
|
379
|
+
assert test_func(5) == 6
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@pytest.mark.asyncio
|
|
383
|
+
async def test_instrument_async_with_defaults():
|
|
384
|
+
"""Test instrument with function that has default arguments."""
|
|
385
|
+
|
|
386
|
+
@instrument
|
|
387
|
+
async def test_func(x: int, y: int = 10) -> int:
|
|
388
|
+
return x + y
|
|
389
|
+
|
|
390
|
+
assert await test_func(5) == 15
|
|
391
|
+
assert await test_func(5, 20) == 25
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_instrument_sync_with_kwargs():
|
|
395
|
+
"""Test instrument with keyword arguments."""
|
|
396
|
+
|
|
397
|
+
@instrument
|
|
398
|
+
def test_func(x: int, **kwargs) -> dict:
|
|
399
|
+
return {"x": x, **kwargs}
|
|
400
|
+
|
|
401
|
+
result = test_func(1, a=2, b=3)
|
|
402
|
+
assert result == {"x": 1, "a": 2, "b": 3}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@pytest.mark.asyncio
|
|
406
|
+
async def test_instrument_async_with_varargs():
|
|
407
|
+
"""Test instrument with *args."""
|
|
408
|
+
|
|
409
|
+
@instrument
|
|
410
|
+
async def test_func(*args) -> int:
|
|
411
|
+
return sum(args)
|
|
412
|
+
|
|
413
|
+
result = await test_func(1, 2, 3, 4)
|
|
414
|
+
assert result == 10
|