hud-python 0.3.5__py3-none-any.whl → 0.4.0__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/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/tools/tests/test_tools.py
CHANGED
|
@@ -1,157 +1,145 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from hud.tools.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
result
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
res = await edit(command="
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@pytest.mark.
|
|
112
|
-
async def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
assert
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@pytest.mark.
|
|
124
|
-
async def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
params = list(sig.parameters.values())
|
|
147
|
-
|
|
148
|
-
assert [p.name for p in params] == ["x", "y"], "*args/**kwargs should be stripped"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def test_build_server_subset():
|
|
152
|
-
"""Ensure build_server registers only requested tools."""
|
|
153
|
-
from hud.tools.helper.mcp_server import build_server
|
|
154
|
-
|
|
155
|
-
mcp = build_server(["bash"])
|
|
156
|
-
names = [t.name for t in asyncio.run(mcp.list_tools())]
|
|
157
|
-
assert names == ["bash"]
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from mcp.types import ImageContent, TextContent
|
|
7
|
+
|
|
8
|
+
from hud.tools.bash import BashTool
|
|
9
|
+
from hud.tools.computer.hud import HudComputerTool
|
|
10
|
+
from hud.tools.edit import EditTool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_bash_tool_echo():
|
|
15
|
+
tool = BashTool()
|
|
16
|
+
|
|
17
|
+
# Monkey-patch the private _session methods so no subprocess is spawned
|
|
18
|
+
from hud.tools.types import ContentResult
|
|
19
|
+
|
|
20
|
+
class _FakeSession:
|
|
21
|
+
async def run(self, cmd: str):
|
|
22
|
+
return ContentResult(output=f"mocked: {cmd}")
|
|
23
|
+
|
|
24
|
+
async def start(self):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
tool.session = _FakeSession() # type: ignore[assignment]
|
|
28
|
+
|
|
29
|
+
result = await tool(command="echo hello")
|
|
30
|
+
assert len(result) > 0
|
|
31
|
+
assert isinstance(result[0], TextContent)
|
|
32
|
+
assert result[0].text == "mocked: echo hello"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_bash_tool_restart_and_no_command():
|
|
37
|
+
from hud.tools.types import ToolError
|
|
38
|
+
|
|
39
|
+
tool = BashTool()
|
|
40
|
+
|
|
41
|
+
from hud.tools.types import ContentResult
|
|
42
|
+
|
|
43
|
+
class _FakeSession:
|
|
44
|
+
async def run(self, cmd: str):
|
|
45
|
+
return ContentResult(output="ran")
|
|
46
|
+
|
|
47
|
+
async def start(self):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def stop(self):
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
tool.session = _FakeSession() # type: ignore[assignment]
|
|
54
|
+
|
|
55
|
+
# Monkey-patch _BashSession.start to avoid launching a real shell
|
|
56
|
+
async def _dummy_start(self):
|
|
57
|
+
self._started = True
|
|
58
|
+
from types import SimpleNamespace
|
|
59
|
+
|
|
60
|
+
# minimal fake process attributes used later
|
|
61
|
+
self._process = SimpleNamespace(returncode=None)
|
|
62
|
+
|
|
63
|
+
import hud.tools.bash as bash_mod
|
|
64
|
+
|
|
65
|
+
bash_mod._BashSession.start = _dummy_start # type: ignore[assignment]
|
|
66
|
+
|
|
67
|
+
# restart=True returns system message
|
|
68
|
+
res = await tool(command="ignored", restart=True)
|
|
69
|
+
# Check that we get content blocks with the restart message
|
|
70
|
+
assert len(res) > 0
|
|
71
|
+
text_blocks = [b for b in res if isinstance(b, TextContent)]
|
|
72
|
+
assert any("restarted" in b.text for b in text_blocks)
|
|
73
|
+
|
|
74
|
+
# Calling without command raises ToolError
|
|
75
|
+
with pytest.raises(ToolError):
|
|
76
|
+
await tool()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
|
|
81
|
+
async def test_edit_tool_flow(tmp_path):
|
|
82
|
+
file_path = tmp_path / "demo.txt"
|
|
83
|
+
|
|
84
|
+
edit = EditTool()
|
|
85
|
+
|
|
86
|
+
# create
|
|
87
|
+
res = await edit(command="create", path=str(file_path), file_text="hello\nworld\n")
|
|
88
|
+
# Check for success message in content blocks
|
|
89
|
+
text_blocks = [b for b in res if isinstance(b, TextContent)]
|
|
90
|
+
assert any("created" in b.text for b in text_blocks)
|
|
91
|
+
|
|
92
|
+
# view
|
|
93
|
+
res = await edit(command="view", path=str(file_path))
|
|
94
|
+
# Check content blocks for file content
|
|
95
|
+
text_blocks = [b for b in res if isinstance(b, TextContent)]
|
|
96
|
+
combined_text = "".join(b.text for b in text_blocks)
|
|
97
|
+
assert "hello" in combined_text
|
|
98
|
+
|
|
99
|
+
# replace
|
|
100
|
+
res = await edit(command="str_replace", path=str(file_path), old_str="world", new_str="earth")
|
|
101
|
+
# Check for success message in content blocks
|
|
102
|
+
text_blocks = [b for b in res if isinstance(b, TextContent)]
|
|
103
|
+
combined_text = "".join(b.text for b in text_blocks)
|
|
104
|
+
assert "has been edited" in combined_text
|
|
105
|
+
|
|
106
|
+
# insert
|
|
107
|
+
res = await edit(command="insert", path=str(file_path), insert_line=1, new_str="first line\n")
|
|
108
|
+
assert res
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_base_executor_simulation():
|
|
113
|
+
from hud.tools.executors.base import BaseExecutor
|
|
114
|
+
|
|
115
|
+
exec = BaseExecutor()
|
|
116
|
+
res = await exec.execute("echo test")
|
|
117
|
+
assert "SIMULATED" in (res.output or "")
|
|
118
|
+
shot = await exec.screenshot()
|
|
119
|
+
assert isinstance(shot, str) and len(shot) > 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.asyncio
|
|
123
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
|
|
124
|
+
async def test_edit_tool_view(tmp_path):
|
|
125
|
+
# Create a temporary file
|
|
126
|
+
p = tmp_path / "sample.txt"
|
|
127
|
+
p.write_text("Sample content\n")
|
|
128
|
+
|
|
129
|
+
tool = EditTool()
|
|
130
|
+
result = await tool(command="view", path=str(p))
|
|
131
|
+
# Check content blocks for file content
|
|
132
|
+
text_blocks = [b for b in result if isinstance(b, TextContent)]
|
|
133
|
+
combined_text = "".join(b.text for b in text_blocks)
|
|
134
|
+
assert "Sample content" in combined_text
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_computer_tool_screenshot():
|
|
139
|
+
comp = HudComputerTool()
|
|
140
|
+
blocks = await comp(action="screenshot")
|
|
141
|
+
# Check that we got content blocks back
|
|
142
|
+
assert blocks is not None
|
|
143
|
+
assert len(blocks) > 0
|
|
144
|
+
# Either ImageContent or TextContent is valid
|
|
145
|
+
assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
|
hud/tools/tests/test_utils.py
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
"""Tests for tools utils."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from unittest.mock import AsyncMock, patch
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from hud.tools.utils import maybe_truncate, run
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TestRun:
|
|
14
|
-
"""Tests for the run function."""
|
|
15
|
-
|
|
16
|
-
@pytest.mark.asyncio
|
|
17
|
-
async def test_run_string_command_success(self):
|
|
18
|
-
"""Test running a string command successfully."""
|
|
19
|
-
mock_proc = AsyncMock()
|
|
20
|
-
mock_proc.returncode = 0
|
|
21
|
-
mock_proc.communicate = AsyncMock(return_value=(b"output", b""))
|
|
22
|
-
|
|
23
|
-
with patch("asyncio.create_subprocess_shell", return_value=mock_proc) as mock_shell:
|
|
24
|
-
return_code, stdout, stderr = await run("echo test")
|
|
25
|
-
|
|
26
|
-
assert return_code == 0
|
|
27
|
-
assert stdout == "output"
|
|
28
|
-
assert stderr == ""
|
|
29
|
-
mock_shell.assert_called_once()
|
|
30
|
-
|
|
31
|
-
@pytest.mark.asyncio
|
|
32
|
-
async def test_run_list_command_success(self):
|
|
33
|
-
"""Test running a list command successfully."""
|
|
34
|
-
mock_proc = AsyncMock()
|
|
35
|
-
mock_proc.returncode = 0
|
|
36
|
-
mock_proc.communicate = AsyncMock(return_value=(b"hello world", b""))
|
|
37
|
-
|
|
38
|
-
with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
|
|
39
|
-
return_code, stdout, stderr = await run(["echo", "hello", "world"])
|
|
40
|
-
|
|
41
|
-
assert return_code == 0
|
|
42
|
-
assert stdout == "hello world"
|
|
43
|
-
assert stderr == ""
|
|
44
|
-
mock_exec.assert_called_once_with(
|
|
45
|
-
"echo",
|
|
46
|
-
"hello",
|
|
47
|
-
"world",
|
|
48
|
-
stdin=None,
|
|
49
|
-
stdout=asyncio.subprocess.PIPE,
|
|
50
|
-
stderr=asyncio.subprocess.PIPE,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
@pytest.mark.asyncio
|
|
54
|
-
async def test_run_with_input(self):
|
|
55
|
-
"""Test running a command with input."""
|
|
56
|
-
mock_proc = AsyncMock()
|
|
57
|
-
mock_proc.returncode = 0
|
|
58
|
-
mock_proc.communicate = AsyncMock(return_value=(b"processed", b""))
|
|
59
|
-
|
|
60
|
-
with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
|
|
61
|
-
return_code, stdout, stderr = await run("cat", input="test input")
|
|
62
|
-
|
|
63
|
-
assert return_code == 0
|
|
64
|
-
assert stdout == "processed"
|
|
65
|
-
mock_proc.communicate.assert_called_once_with(input=b"test input")
|
|
66
|
-
|
|
67
|
-
@pytest.mark.asyncio
|
|
68
|
-
async def test_run_with_error(self):
|
|
69
|
-
"""Test running a command that returns an error."""
|
|
70
|
-
mock_proc = AsyncMock()
|
|
71
|
-
mock_proc.returncode = 1
|
|
72
|
-
mock_proc.communicate = AsyncMock(return_value=(b"", b"error message"))
|
|
73
|
-
|
|
74
|
-
with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
|
|
75
|
-
return_code, stdout, stderr = await run("false")
|
|
76
|
-
|
|
77
|
-
assert return_code == 1
|
|
78
|
-
assert stdout == ""
|
|
79
|
-
assert stderr == "error message"
|
|
80
|
-
|
|
81
|
-
@pytest.mark.asyncio
|
|
82
|
-
async def test_run_with_timeout(self):
|
|
83
|
-
"""Test running a command with custom timeout."""
|
|
84
|
-
mock_proc = AsyncMock()
|
|
85
|
-
mock_proc.returncode = 0
|
|
86
|
-
mock_proc.communicate = AsyncMock(return_value=(b"done", b""))
|
|
87
|
-
|
|
88
|
-
with (
|
|
89
|
-
patch("asyncio.create_subprocess_shell", return_value=mock_proc),
|
|
90
|
-
patch("asyncio.wait_for") as mock_wait_for,
|
|
91
|
-
):
|
|
92
|
-
mock_wait_for.return_value = (b"done", b"")
|
|
93
|
-
|
|
94
|
-
return_code, stdout, stderr = await run("sleep 1", timeout=5.0)
|
|
95
|
-
|
|
96
|
-
# Check that wait_for was called with the correct timeout
|
|
97
|
-
mock_wait_for.assert_called_once()
|
|
98
|
-
assert mock_wait_for.call_args[1]["timeout"] == 5.0
|
|
99
|
-
|
|
100
|
-
@pytest.mark.asyncio
|
|
101
|
-
async def test_run_timeout_exception(self):
|
|
102
|
-
"""Test running a command that times out."""
|
|
103
|
-
mock_proc = AsyncMock()
|
|
104
|
-
|
|
105
|
-
with (
|
|
106
|
-
patch("asyncio.create_subprocess_shell", return_value=mock_proc),
|
|
107
|
-
patch("asyncio.wait_for", side_effect=TimeoutError()),
|
|
108
|
-
pytest.raises(asyncio.TimeoutError),
|
|
109
|
-
):
|
|
110
|
-
await run("sleep infinity", timeout=0.1)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class TestMaybeTruncate:
|
|
114
|
-
"""Tests for the maybe_truncate function."""
|
|
115
|
-
|
|
116
|
-
def test_maybe_truncate_short_text(self):
|
|
117
|
-
"""Test that short text is not truncated."""
|
|
118
|
-
text = "This is a short text"
|
|
119
|
-
result = maybe_truncate(text)
|
|
120
|
-
assert result == text
|
|
121
|
-
|
|
122
|
-
def test_maybe_truncate_long_text_default(self):
|
|
123
|
-
"""Test that long text is truncated with default limit."""
|
|
124
|
-
text = "x" * 30000 # Much longer than default limit
|
|
125
|
-
result = maybe_truncate(text)
|
|
126
|
-
|
|
127
|
-
assert len(result) < len(text)
|
|
128
|
-
assert result.endswith("... (truncated)")
|
|
129
|
-
assert len(result) == 20480 + len("... (truncated)")
|
|
130
|
-
|
|
131
|
-
def test_maybe_truncate_custom_limit(self):
|
|
132
|
-
"""Test truncation with custom limit."""
|
|
133
|
-
text = "abcdefghijklmnopqrstuvwxyz"
|
|
134
|
-
result = maybe_truncate(text, max_length=10)
|
|
135
|
-
|
|
136
|
-
assert result == "abcdefghij... (truncated)"
|
|
137
|
-
|
|
138
|
-
def test_maybe_truncate_exact_limit(self):
|
|
139
|
-
"""Test text exactly at limit is not truncated."""
|
|
140
|
-
text = "x" * 100
|
|
141
|
-
result = maybe_truncate(text, max_length=100)
|
|
142
|
-
|
|
143
|
-
assert result == text
|
|
144
|
-
|
|
145
|
-
def test_maybe_truncate_empty_string(self):
|
|
146
|
-
"""Test empty string handling."""
|
|
147
|
-
result = maybe_truncate("")
|
|
148
|
-
assert result == ""
|
|
149
|
-
|
|
150
|
-
def test_maybe_truncate_unicode(self):
|
|
151
|
-
"""Test truncation with unicode characters."""
|
|
152
|
-
text = "🎉" * 5000
|
|
153
|
-
result = maybe_truncate(text, max_length=10)
|
|
154
|
-
|
|
155
|
-
assert len(result) > 10 # Because of "... (truncated)" suffix
|
|
156
|
-
assert result.endswith("... (truncated)")
|
|
1
|
+
"""Tests for tools utils."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from unittest.mock import AsyncMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from hud.tools.utils import maybe_truncate, run
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestRun:
|
|
14
|
+
"""Tests for the run function."""
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_run_string_command_success(self):
|
|
18
|
+
"""Test running a string command successfully."""
|
|
19
|
+
mock_proc = AsyncMock()
|
|
20
|
+
mock_proc.returncode = 0
|
|
21
|
+
mock_proc.communicate = AsyncMock(return_value=(b"output", b""))
|
|
22
|
+
|
|
23
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_proc) as mock_shell:
|
|
24
|
+
return_code, stdout, stderr = await run("echo test")
|
|
25
|
+
|
|
26
|
+
assert return_code == 0
|
|
27
|
+
assert stdout == "output"
|
|
28
|
+
assert stderr == ""
|
|
29
|
+
mock_shell.assert_called_once()
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_run_list_command_success(self):
|
|
33
|
+
"""Test running a list command successfully."""
|
|
34
|
+
mock_proc = AsyncMock()
|
|
35
|
+
mock_proc.returncode = 0
|
|
36
|
+
mock_proc.communicate = AsyncMock(return_value=(b"hello world", b""))
|
|
37
|
+
|
|
38
|
+
with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
|
|
39
|
+
return_code, stdout, stderr = await run(["echo", "hello", "world"])
|
|
40
|
+
|
|
41
|
+
assert return_code == 0
|
|
42
|
+
assert stdout == "hello world"
|
|
43
|
+
assert stderr == ""
|
|
44
|
+
mock_exec.assert_called_once_with(
|
|
45
|
+
"echo",
|
|
46
|
+
"hello",
|
|
47
|
+
"world",
|
|
48
|
+
stdin=None,
|
|
49
|
+
stdout=asyncio.subprocess.PIPE,
|
|
50
|
+
stderr=asyncio.subprocess.PIPE,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_run_with_input(self):
|
|
55
|
+
"""Test running a command with input."""
|
|
56
|
+
mock_proc = AsyncMock()
|
|
57
|
+
mock_proc.returncode = 0
|
|
58
|
+
mock_proc.communicate = AsyncMock(return_value=(b"processed", b""))
|
|
59
|
+
|
|
60
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
|
|
61
|
+
return_code, stdout, stderr = await run("cat", input="test input")
|
|
62
|
+
|
|
63
|
+
assert return_code == 0
|
|
64
|
+
assert stdout == "processed"
|
|
65
|
+
mock_proc.communicate.assert_called_once_with(input=b"test input")
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_run_with_error(self):
|
|
69
|
+
"""Test running a command that returns an error."""
|
|
70
|
+
mock_proc = AsyncMock()
|
|
71
|
+
mock_proc.returncode = 1
|
|
72
|
+
mock_proc.communicate = AsyncMock(return_value=(b"", b"error message"))
|
|
73
|
+
|
|
74
|
+
with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
|
|
75
|
+
return_code, stdout, stderr = await run("false")
|
|
76
|
+
|
|
77
|
+
assert return_code == 1
|
|
78
|
+
assert stdout == ""
|
|
79
|
+
assert stderr == "error message"
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_run_with_timeout(self):
|
|
83
|
+
"""Test running a command with custom timeout."""
|
|
84
|
+
mock_proc = AsyncMock()
|
|
85
|
+
mock_proc.returncode = 0
|
|
86
|
+
mock_proc.communicate = AsyncMock(return_value=(b"done", b""))
|
|
87
|
+
|
|
88
|
+
with (
|
|
89
|
+
patch("asyncio.create_subprocess_shell", return_value=mock_proc),
|
|
90
|
+
patch("asyncio.wait_for") as mock_wait_for,
|
|
91
|
+
):
|
|
92
|
+
mock_wait_for.return_value = (b"done", b"")
|
|
93
|
+
|
|
94
|
+
return_code, stdout, stderr = await run("sleep 1", timeout=5.0)
|
|
95
|
+
|
|
96
|
+
# Check that wait_for was called with the correct timeout
|
|
97
|
+
mock_wait_for.assert_called_once()
|
|
98
|
+
assert mock_wait_for.call_args[1]["timeout"] == 5.0
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_run_timeout_exception(self):
|
|
102
|
+
"""Test running a command that times out."""
|
|
103
|
+
mock_proc = AsyncMock()
|
|
104
|
+
|
|
105
|
+
with (
|
|
106
|
+
patch("asyncio.create_subprocess_shell", return_value=mock_proc),
|
|
107
|
+
patch("asyncio.wait_for", side_effect=TimeoutError()),
|
|
108
|
+
pytest.raises(asyncio.TimeoutError),
|
|
109
|
+
):
|
|
110
|
+
await run("sleep infinity", timeout=0.1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestMaybeTruncate:
|
|
114
|
+
"""Tests for the maybe_truncate function."""
|
|
115
|
+
|
|
116
|
+
def test_maybe_truncate_short_text(self):
|
|
117
|
+
"""Test that short text is not truncated."""
|
|
118
|
+
text = "This is a short text"
|
|
119
|
+
result = maybe_truncate(text)
|
|
120
|
+
assert result == text
|
|
121
|
+
|
|
122
|
+
def test_maybe_truncate_long_text_default(self):
|
|
123
|
+
"""Test that long text is truncated with default limit."""
|
|
124
|
+
text = "x" * 30000 # Much longer than default limit
|
|
125
|
+
result = maybe_truncate(text)
|
|
126
|
+
|
|
127
|
+
assert len(result) < len(text)
|
|
128
|
+
assert result.endswith("... (truncated)")
|
|
129
|
+
assert len(result) == 20480 + len("... (truncated)")
|
|
130
|
+
|
|
131
|
+
def test_maybe_truncate_custom_limit(self):
|
|
132
|
+
"""Test truncation with custom limit."""
|
|
133
|
+
text = "abcdefghijklmnopqrstuvwxyz"
|
|
134
|
+
result = maybe_truncate(text, max_length=10)
|
|
135
|
+
|
|
136
|
+
assert result == "abcdefghij... (truncated)"
|
|
137
|
+
|
|
138
|
+
def test_maybe_truncate_exact_limit(self):
|
|
139
|
+
"""Test text exactly at limit is not truncated."""
|
|
140
|
+
text = "x" * 100
|
|
141
|
+
result = maybe_truncate(text, max_length=100)
|
|
142
|
+
|
|
143
|
+
assert result == text
|
|
144
|
+
|
|
145
|
+
def test_maybe_truncate_empty_string(self):
|
|
146
|
+
"""Test empty string handling."""
|
|
147
|
+
result = maybe_truncate("")
|
|
148
|
+
assert result == ""
|
|
149
|
+
|
|
150
|
+
def test_maybe_truncate_unicode(self):
|
|
151
|
+
"""Test truncation with unicode characters."""
|
|
152
|
+
text = "🎉" * 5000
|
|
153
|
+
result = maybe_truncate(text, max_length=10)
|
|
154
|
+
|
|
155
|
+
assert len(result) > 10 # Because of "... (truncated)" suffix
|
|
156
|
+
assert result.endswith("... (truncated)")
|