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,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hud.cli.utils.package_runner import run_package_as_mcp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
@mock.patch("hud.cli.utils.package_runner.FastMCP")
|
|
13
|
+
async def test_run_package_as_external_command(MockFastMCP):
|
|
14
|
+
proxy = mock.AsyncMock()
|
|
15
|
+
MockFastMCP.as_proxy.return_value = proxy
|
|
16
|
+
await run_package_as_mcp(["python", "-m", "server"], transport="http", port=9999)
|
|
17
|
+
assert proxy.run_async.awaited
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
@mock.patch("hud.cli.utils.package_runner.importlib.import_module")
|
|
22
|
+
async def test_run_package_import_module(mock_import):
|
|
23
|
+
server = SimpleNamespace(name="test", run_async=mock.AsyncMock())
|
|
24
|
+
mod = SimpleNamespace(mcp=server)
|
|
25
|
+
mock_import.return_value = mod
|
|
26
|
+
await run_package_as_mcp("module_name", transport="stdio")
|
|
27
|
+
assert server.run_async.awaited
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
@mock.patch("hud.cli.utils.package_runner.importlib.import_module")
|
|
32
|
+
async def test_run_package_import_missing_attr(mock_import):
|
|
33
|
+
mock_import.return_value = SimpleNamespace()
|
|
34
|
+
with pytest.raises(SystemExit):
|
|
35
|
+
await run_package_as_mcp("module_name", transport="stdio")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from hud.cli.utils.registry import (
|
|
6
|
+
extract_digest_from_image,
|
|
7
|
+
extract_name_and_tag,
|
|
8
|
+
list_registry_entries,
|
|
9
|
+
load_from_registry,
|
|
10
|
+
save_to_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_extract_digest_from_image_variants():
|
|
18
|
+
assert extract_digest_from_image("repo/name@sha256:abcdef1234567890") == "abcdef123456"
|
|
19
|
+
assert extract_digest_from_image("sha256:deadbeefcafebabe") == "deadbeefcafe"
|
|
20
|
+
assert extract_digest_from_image("org/name:tag") == "tag"
|
|
21
|
+
assert extract_digest_from_image("org/name") == "latest"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_extract_name_and_tag():
|
|
25
|
+
assert extract_name_and_tag("docker.io/hudpython/test_init:latest@sha256:abc") == (
|
|
26
|
+
"hudpython/test_init",
|
|
27
|
+
"latest",
|
|
28
|
+
)
|
|
29
|
+
assert extract_name_and_tag("myorg/myenv:v1.0") == ("myorg/myenv", "v1.0")
|
|
30
|
+
assert extract_name_and_tag("myorg/myenv") == ("myorg/myenv", "latest")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_save_load_list_registry(tmp_path: Path, monkeypatch):
|
|
34
|
+
# Redirect registry dir to temp
|
|
35
|
+
from hud.cli.utils import registry as mod
|
|
36
|
+
|
|
37
|
+
monkeypatch.setattr(mod, "get_registry_dir", lambda: tmp_path)
|
|
38
|
+
|
|
39
|
+
data = {"image": "org/name:tag", "build": {"version": "0.1.0"}}
|
|
40
|
+
saved = save_to_registry(data, "org/name:tag@sha256:abcdef0123456789", verbose=True)
|
|
41
|
+
assert saved is not None and saved.exists()
|
|
42
|
+
|
|
43
|
+
# Digest directory was created
|
|
44
|
+
entries = list_registry_entries()
|
|
45
|
+
assert len(entries) == 1
|
|
46
|
+
|
|
47
|
+
digest, _ = entries[0]
|
|
48
|
+
loaded = load_from_registry(digest)
|
|
49
|
+
assert loaded and loaded.get("image") == "org/name:tag"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from hud.cli.utils.remote_runner import run_remote_server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_run_remote_server_requires_api_key(monkeypatch):
|
|
9
|
+
# Ensure settings.api_key is None and no api_key provided
|
|
10
|
+
from hud.cli.utils import remote_runner as mod
|
|
11
|
+
|
|
12
|
+
monkeypatch.setattr(mod.settings, "api_key", None, raising=True)
|
|
13
|
+
|
|
14
|
+
with pytest.raises(SystemExit) as exc:
|
|
15
|
+
run_remote_server(
|
|
16
|
+
image="img:latest",
|
|
17
|
+
docker_args=[],
|
|
18
|
+
transport="stdio",
|
|
19
|
+
port=8765,
|
|
20
|
+
url="https://api.example.com/mcp",
|
|
21
|
+
api_key=None,
|
|
22
|
+
run_id=None,
|
|
23
|
+
verbose=False,
|
|
24
|
+
)
|
|
25
|
+
assert exc.value.code == 1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest import mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.cli.utils.remote_runner import build_remote_headers, parse_env_vars, parse_headers
|
|
8
|
+
from hud.cli.utils.runner import run_mcp_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parse_headers_and_env_vars():
|
|
12
|
+
assert parse_headers(["A:B", "C=D"]) == {"A": "B", "C": "D"}
|
|
13
|
+
assert parse_env_vars(["API_KEY=xxx"]) == {"Env-Api-Key": "xxx"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_build_remote_headers_combines():
|
|
17
|
+
headers = build_remote_headers(
|
|
18
|
+
image="img:latest", env_args=["X=1"], header_args=["H:V"], api_key="k", run_id="r"
|
|
19
|
+
)
|
|
20
|
+
assert headers["Mcp-Image"] == "img:latest"
|
|
21
|
+
assert headers["Authorization"].startswith("Bearer ")
|
|
22
|
+
assert headers["Run-Id"] == "r"
|
|
23
|
+
assert headers["Env-X"] == "1"
|
|
24
|
+
assert headers["H"] == "V"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mock.patch("hud.cli.utils.runner.run_stdio_server")
|
|
28
|
+
def test_run_mcp_server_stdio(mock_stdio):
|
|
29
|
+
run_mcp_server("img", [], "stdio", 8765, verbose=False, interactive=False)
|
|
30
|
+
assert mock_stdio.called
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_run_mcp_server_stdio_interactive_fails():
|
|
34
|
+
with pytest.raises(SystemExit):
|
|
35
|
+
run_mcp_server("img", [], "stdio", 8765, verbose=False, interactive=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@mock.patch("hud.cli.utils.runner.run_http_server")
|
|
39
|
+
def test_run_mcp_server_http(mock_http):
|
|
40
|
+
run_mcp_server("img", [], "http", 8765, verbose=True, interactive=False)
|
|
41
|
+
assert mock_http.called
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@mock.patch("hud.cli.utils.runner.run_http_server_interactive")
|
|
45
|
+
def test_run_mcp_server_http_interactive(mock_http_int):
|
|
46
|
+
run_mcp_server("img", [], "http", 8765, verbose=False, interactive=True)
|
|
47
|
+
assert mock_http_int.called
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_run_mcp_server_unknown():
|
|
51
|
+
with pytest.raises(SystemExit):
|
|
52
|
+
run_mcp_server("img", [], "bad", 8765, verbose=False)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from hud.cli.utils.source_hash import compute_source_hash, list_source_files
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_source_hash_changes_with_content(tmp_path: Path):
|
|
12
|
+
env = tmp_path / "env"
|
|
13
|
+
env.mkdir()
|
|
14
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
15
|
+
(env / "pyproject.toml").write_text("[tool.hud]\n")
|
|
16
|
+
(env / "server").mkdir()
|
|
17
|
+
(env / "server" / "main.py").write_text("print('hi')\n")
|
|
18
|
+
|
|
19
|
+
h1 = compute_source_hash(env)
|
|
20
|
+
# Change file content
|
|
21
|
+
(env / "server" / "main.py").write_text("print('bye')\n")
|
|
22
|
+
h2 = compute_source_hash(env)
|
|
23
|
+
assert h1 != h2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_list_source_files_sorted(tmp_path: Path):
|
|
27
|
+
env = tmp_path / "env"
|
|
28
|
+
env.mkdir()
|
|
29
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
30
|
+
(env / "environment").mkdir()
|
|
31
|
+
(env / "environment" / "a.py").write_text("a")
|
|
32
|
+
(env / "environment" / "b.py").write_text("b")
|
|
33
|
+
|
|
34
|
+
files = list_source_files(env)
|
|
35
|
+
rels = [str(p.resolve().relative_to(env)).replace("\\", "/") for p in files]
|
|
36
|
+
assert rels == ["Dockerfile", "environment/a.py", "environment/b.py"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hud.cli.utils.tasks import find_tasks_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@patch("pathlib.Path.cwd")
|
|
12
|
+
def test_find_tasks_file_with_arg(mock_cwd):
|
|
13
|
+
"""Test that when a tasks file is provided, it's returned as-is."""
|
|
14
|
+
assert find_tasks_file("some/path.json") == "some/path.json"
|
|
15
|
+
mock_cwd.assert_not_called()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@patch("pathlib.Path.cwd")
|
|
19
|
+
def test_find_tasks_file_no_files(mock_cwd):
|
|
20
|
+
"""Test that FileNotFoundError is raised when no task files exist."""
|
|
21
|
+
mock_path = MagicMock(spec=Path)
|
|
22
|
+
mock_path.glob.return_value = []
|
|
23
|
+
mock_cwd.return_value = mock_path
|
|
24
|
+
|
|
25
|
+
with pytest.raises(FileNotFoundError, match="No task JSON or JSONL files found"):
|
|
26
|
+
find_tasks_file(None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@patch("hud.cli.utils.tasks.hud_console")
|
|
30
|
+
@patch("pathlib.Path.cwd")
|
|
31
|
+
def test_find_tasks_file_single_file(mock_cwd, mock_console):
|
|
32
|
+
"""Test that when only one file exists, it's returned without prompting."""
|
|
33
|
+
mock_path = MagicMock(spec=Path)
|
|
34
|
+
mock_file = MagicMock(spec=Path)
|
|
35
|
+
mock_file.__str__.return_value = "test.json"
|
|
36
|
+
|
|
37
|
+
def glob_side_effect(pattern):
|
|
38
|
+
if pattern == "*.json":
|
|
39
|
+
return [mock_file]
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
mock_path.glob.side_effect = glob_side_effect
|
|
43
|
+
mock_path.__str__.return_value = str(Path.cwd())
|
|
44
|
+
mock_cwd.return_value = mock_path
|
|
45
|
+
|
|
46
|
+
result = find_tasks_file(None)
|
|
47
|
+
assert result == "test.json"
|
|
48
|
+
mock_console.select.assert_not_called()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@patch("hud.cli.utils.tasks.hud_console")
|
|
52
|
+
@patch("pathlib.Path.cwd")
|
|
53
|
+
def test_find_tasks_file_multiple_files(mock_cwd, mock_console):
|
|
54
|
+
"""Test that when multiple files exist, user is prompted to select one."""
|
|
55
|
+
mock_path = MagicMock(spec=Path)
|
|
56
|
+
mock_file1 = MagicMock(spec=Path)
|
|
57
|
+
mock_file1.__str__.return_value = "test1.json"
|
|
58
|
+
mock_file2 = MagicMock(spec=Path)
|
|
59
|
+
mock_file2.__str__.return_value = "test2.jsonl"
|
|
60
|
+
|
|
61
|
+
def glob_side_effect(pattern):
|
|
62
|
+
if pattern == "*.json":
|
|
63
|
+
return [mock_file1]
|
|
64
|
+
if pattern == "*.jsonl":
|
|
65
|
+
return [mock_file2]
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
mock_path.glob.side_effect = glob_side_effect
|
|
69
|
+
mock_path.__str__.return_value = str(Path.cwd())
|
|
70
|
+
mock_cwd.return_value = mock_path
|
|
71
|
+
mock_console.select.return_value = "test2.jsonl"
|
|
72
|
+
|
|
73
|
+
result = find_tasks_file(None)
|
|
74
|
+
|
|
75
|
+
assert result == "test2.jsonl"
|
|
76
|
+
mock_console.select.assert_called_once()
|
|
77
|
+
call_args = mock_console.select.call_args
|
|
78
|
+
assert call_args[0][0] == "Select a tasks file"
|
|
79
|
+
assert "test1.json" in call_args[1]["choices"]
|
|
80
|
+
assert "test2.jsonl" in call_args[1]["choices"]
|
hud/cli/utils/version_check.py
CHANGED
|
@@ -171,11 +171,11 @@ def check_for_updates() -> VersionInfo | None:
|
|
|
171
171
|
|
|
172
172
|
# Try to load from cache
|
|
173
173
|
cached_info = _load_cache()
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
# If cache exists but current version has changed (user upgraded), invalidate cache
|
|
176
176
|
if cached_info and cached_info.current != current:
|
|
177
177
|
cached_info = None # Force fresh check
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
if cached_info:
|
|
180
180
|
# Update the current version in the cached info to reflect reality
|
|
181
181
|
# but keep the cached latest version and timestamp
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.datasets.runner import _flush_telemetry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_flush_telemetry():
|
|
12
|
+
"""Test _flush_telemetry function."""
|
|
13
|
+
with (
|
|
14
|
+
patch("hud.otel.config.is_telemetry_configured", return_value=True),
|
|
15
|
+
patch("hud.utils.hud_console.hud_console"),
|
|
16
|
+
patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
|
|
17
|
+
patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
|
|
18
|
+
):
|
|
19
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
20
|
+
|
|
21
|
+
mock_provider = MagicMock(spec=TracerProvider)
|
|
22
|
+
mock_provider.force_flush.return_value = True
|
|
23
|
+
mock_get_provider.return_value = mock_provider
|
|
24
|
+
|
|
25
|
+
mock_wait.return_value = 5
|
|
26
|
+
|
|
27
|
+
await _flush_telemetry()
|
|
28
|
+
|
|
29
|
+
mock_wait.assert_called_once()
|
|
30
|
+
mock_provider.force_flush.assert_called_once_with(timeout_millis=20000)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_flush_telemetry_no_telemetry():
|
|
35
|
+
"""Test _flush_telemetry when telemetry is not configured."""
|
|
36
|
+
with (
|
|
37
|
+
patch("hud.otel.config.is_telemetry_configured", return_value=False),
|
|
38
|
+
patch("hud.utils.hud_console.hud_console"),
|
|
39
|
+
patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
|
|
40
|
+
patch("opentelemetry.trace.get_tracer_provider"),
|
|
41
|
+
):
|
|
42
|
+
mock_wait.return_value = 0
|
|
43
|
+
|
|
44
|
+
await _flush_telemetry()
|
|
45
|
+
|
|
46
|
+
mock_wait.assert_called_once()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_flush_telemetry_exception():
|
|
51
|
+
"""Test _flush_telemetry handles exceptions gracefully."""
|
|
52
|
+
with (
|
|
53
|
+
patch("hud.otel.config.is_telemetry_configured", return_value=True),
|
|
54
|
+
patch("hud.utils.hud_console.hud_console"),
|
|
55
|
+
patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
|
|
56
|
+
patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
|
|
57
|
+
):
|
|
58
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
59
|
+
|
|
60
|
+
mock_provider = MagicMock(spec=TracerProvider)
|
|
61
|
+
mock_provider.force_flush.side_effect = Exception("Flush failed")
|
|
62
|
+
mock_get_provider.return_value = mock_provider
|
|
63
|
+
|
|
64
|
+
mock_wait.return_value = 3
|
|
65
|
+
|
|
66
|
+
# Should not raise
|
|
67
|
+
await _flush_telemetry()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_flush_telemetry_no_completed_tasks():
|
|
72
|
+
"""Test _flush_telemetry when no tasks were completed."""
|
|
73
|
+
with (
|
|
74
|
+
patch("hud.otel.config.is_telemetry_configured", return_value=True),
|
|
75
|
+
patch("hud.utils.hud_console.hud_console"),
|
|
76
|
+
patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
|
|
77
|
+
patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
|
|
78
|
+
):
|
|
79
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
80
|
+
|
|
81
|
+
mock_provider = MagicMock(spec=TracerProvider)
|
|
82
|
+
mock_get_provider.return_value = mock_provider
|
|
83
|
+
|
|
84
|
+
mock_wait.return_value = 0
|
|
85
|
+
|
|
86
|
+
await _flush_telemetry()
|
|
87
|
+
|
|
88
|
+
mock_provider.force_flush.assert_called_once()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_flush_telemetry_non_sdk_provider():
|
|
93
|
+
"""Test _flush_telemetry with non-SDK TracerProvider."""
|
|
94
|
+
with (
|
|
95
|
+
patch("hud.otel.config.is_telemetry_configured", return_value=True),
|
|
96
|
+
patch("hud.utils.hud_console.hud_console"),
|
|
97
|
+
patch("hud.utils.task_tracking.wait_all_tasks", new_callable=AsyncMock) as mock_wait,
|
|
98
|
+
patch("opentelemetry.trace.get_tracer_provider") as mock_get_provider,
|
|
99
|
+
):
|
|
100
|
+
# Return a non-TracerProvider object
|
|
101
|
+
mock_get_provider.return_value = MagicMock(spec=object)
|
|
102
|
+
|
|
103
|
+
mock_wait.return_value = 2
|
|
104
|
+
|
|
105
|
+
# Should not raise
|
|
106
|
+
await _flush_telemetry()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hud.datasets.utils import fetch_system_prompt_from_dataset, save_tasks
|
|
8
|
+
from hud.types import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_fetch_system_prompt_success():
|
|
13
|
+
"""Test successful fetch of system prompt."""
|
|
14
|
+
with patch("huggingface_hub.hf_hub_download") as mock_download:
|
|
15
|
+
mock_download.return_value = "/tmp/system_prompt.txt"
|
|
16
|
+
with patch("builtins.open", mock_open(read_data="Test system prompt")):
|
|
17
|
+
result = await fetch_system_prompt_from_dataset("test/dataset")
|
|
18
|
+
assert result == "Test system prompt"
|
|
19
|
+
mock_download.assert_called_once()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_fetch_system_prompt_empty_file():
|
|
24
|
+
"""Test fetch when file is empty."""
|
|
25
|
+
with patch("huggingface_hub.hf_hub_download") as mock_download:
|
|
26
|
+
mock_download.return_value = "/tmp/system_prompt.txt"
|
|
27
|
+
with patch("builtins.open", mock_open(read_data=" \n ")):
|
|
28
|
+
result = await fetch_system_prompt_from_dataset("test/dataset")
|
|
29
|
+
assert result is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_fetch_system_prompt_file_not_found():
|
|
34
|
+
"""Test fetch when file doesn't exist."""
|
|
35
|
+
with patch("huggingface_hub.hf_hub_download") as mock_download:
|
|
36
|
+
from huggingface_hub.errors import EntryNotFoundError
|
|
37
|
+
|
|
38
|
+
mock_download.side_effect = EntryNotFoundError("File not found")
|
|
39
|
+
result = await fetch_system_prompt_from_dataset("test/dataset")
|
|
40
|
+
assert result is None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_fetch_system_prompt_import_error():
|
|
45
|
+
"""Test fetch when huggingface_hub is not installed."""
|
|
46
|
+
# Mock the import itself to raise ImportError
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
with patch.dict(sys.modules, {"huggingface_hub": None}):
|
|
50
|
+
result = await fetch_system_prompt_from_dataset("test/dataset")
|
|
51
|
+
assert result is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_fetch_system_prompt_general_exception():
|
|
56
|
+
"""Test fetch with general exception."""
|
|
57
|
+
with patch("huggingface_hub.hf_hub_download") as mock_download:
|
|
58
|
+
mock_download.side_effect = Exception("Network error")
|
|
59
|
+
result = await fetch_system_prompt_from_dataset("test/dataset")
|
|
60
|
+
assert result is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_save_tasks_basic():
|
|
64
|
+
"""Test basic save_tasks functionality."""
|
|
65
|
+
tasks = [
|
|
66
|
+
{"id": "1", "prompt": "test", "mcp_config": {"key": "value"}},
|
|
67
|
+
{"id": "2", "prompt": "test2", "mcp_config": {"key2": "value2"}},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
71
|
+
mock_dataset = MagicMock()
|
|
72
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
73
|
+
|
|
74
|
+
save_tasks(tasks, "test/repo")
|
|
75
|
+
|
|
76
|
+
mock_dataset_class.from_list.assert_called_once()
|
|
77
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
78
|
+
assert len(call_args) == 2
|
|
79
|
+
# Check that mcp_config was JSON serialized
|
|
80
|
+
assert isinstance(call_args[0]["mcp_config"], str)
|
|
81
|
+
mock_dataset.push_to_hub.assert_called_once_with("test/repo")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_save_tasks_with_specific_fields():
|
|
85
|
+
"""Test save_tasks with specific fields."""
|
|
86
|
+
tasks = [
|
|
87
|
+
{"id": "1", "prompt": "test", "mcp_config": {"key": "value"}, "extra": "data"},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
91
|
+
mock_dataset = MagicMock()
|
|
92
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
93
|
+
|
|
94
|
+
save_tasks(tasks, "test/repo", fields=["id", "prompt"])
|
|
95
|
+
|
|
96
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
97
|
+
assert "id" in call_args[0]
|
|
98
|
+
assert "prompt" in call_args[0]
|
|
99
|
+
assert "extra" not in call_args[0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_save_tasks_with_list_field():
|
|
103
|
+
"""Test save_tasks serializes list fields."""
|
|
104
|
+
tasks = [
|
|
105
|
+
{"id": "1", "tags": ["tag1", "tag2"], "count": 5},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
109
|
+
mock_dataset = MagicMock()
|
|
110
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
111
|
+
|
|
112
|
+
save_tasks(tasks, "test/repo")
|
|
113
|
+
|
|
114
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
115
|
+
# List should be JSON serialized
|
|
116
|
+
assert isinstance(call_args[0]["tags"], str)
|
|
117
|
+
assert '"tag1"' in call_args[0]["tags"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_save_tasks_with_primitive_types():
|
|
121
|
+
"""Test save_tasks handles various primitive types."""
|
|
122
|
+
tasks = [
|
|
123
|
+
{
|
|
124
|
+
"string": "text",
|
|
125
|
+
"integer": 42,
|
|
126
|
+
"float": 3.14,
|
|
127
|
+
"boolean": True,
|
|
128
|
+
"none": None,
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
133
|
+
mock_dataset = MagicMock()
|
|
134
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
135
|
+
|
|
136
|
+
save_tasks(tasks, "test/repo")
|
|
137
|
+
|
|
138
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
139
|
+
assert call_args[0]["string"] == "text"
|
|
140
|
+
assert call_args[0]["integer"] == 42
|
|
141
|
+
assert call_args[0]["float"] == 3.14
|
|
142
|
+
assert call_args[0]["boolean"] is True
|
|
143
|
+
assert call_args[0]["none"] == "" # None becomes empty string
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_save_tasks_with_other_type():
|
|
147
|
+
"""Test save_tasks converts other types to string."""
|
|
148
|
+
|
|
149
|
+
class CustomObj:
|
|
150
|
+
def __str__(self):
|
|
151
|
+
return "custom_value"
|
|
152
|
+
|
|
153
|
+
tasks = [
|
|
154
|
+
{"id": "1", "custom": CustomObj()},
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
158
|
+
mock_dataset = MagicMock()
|
|
159
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
160
|
+
|
|
161
|
+
save_tasks(tasks, "test/repo")
|
|
162
|
+
|
|
163
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
164
|
+
assert call_args[0]["custom"] == "custom_value"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_save_tasks_rejects_task_objects():
|
|
168
|
+
"""Test save_tasks raises error for Task objects."""
|
|
169
|
+
task = Task(prompt="test", mcp_config={})
|
|
170
|
+
|
|
171
|
+
with pytest.raises(ValueError, match="expects dictionaries, not Task objects"):
|
|
172
|
+
save_tasks([task], "test/repo") # type: ignore
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_save_tasks_rejects_task_objects_in_list():
|
|
176
|
+
"""Test save_tasks raises error when Task object is in the list."""
|
|
177
|
+
tasks = [
|
|
178
|
+
{"id": "1", "prompt": "test", "mcp_config": {}},
|
|
179
|
+
Task(prompt="test2", mcp_config={}), # Task object
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
with pytest.raises(ValueError, match="Item 1 is a Task object"):
|
|
183
|
+
save_tasks(tasks, "test/repo") # type: ignore
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_save_tasks_with_kwargs():
|
|
187
|
+
"""Test save_tasks passes kwargs to push_to_hub."""
|
|
188
|
+
tasks = [{"id": "1", "prompt": "test"}]
|
|
189
|
+
|
|
190
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
191
|
+
mock_dataset = MagicMock()
|
|
192
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
193
|
+
|
|
194
|
+
save_tasks(tasks, "test/repo", private=True, commit_message="Test commit")
|
|
195
|
+
|
|
196
|
+
mock_dataset.push_to_hub.assert_called_once_with(
|
|
197
|
+
"test/repo", private=True, commit_message="Test commit"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_save_tasks_field_not_in_dict():
|
|
202
|
+
"""Test save_tasks handles missing fields gracefully."""
|
|
203
|
+
tasks = [
|
|
204
|
+
{"id": "1", "prompt": "test"},
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
208
|
+
mock_dataset = MagicMock()
|
|
209
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
210
|
+
|
|
211
|
+
# Request fields that don't exist
|
|
212
|
+
save_tasks(tasks, "test/repo", fields=["id", "missing_field"])
|
|
213
|
+
|
|
214
|
+
call_args = mock_dataset_class.from_list.call_args[0][0]
|
|
215
|
+
assert "id" in call_args[0]
|
|
216
|
+
assert "missing_field" not in call_args[0]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_save_tasks_empty_list():
|
|
220
|
+
"""Test save_tasks with empty list."""
|
|
221
|
+
with patch("hud.datasets.utils.Dataset") as mock_dataset_class:
|
|
222
|
+
mock_dataset = MagicMock()
|
|
223
|
+
mock_dataset_class.from_list.return_value = mock_dataset
|
|
224
|
+
|
|
225
|
+
save_tasks([], "test/repo")
|
|
226
|
+
|
|
227
|
+
mock_dataset_class.from_list.assert_called_once_with([])
|
|
228
|
+
mock_dataset.push_to_hub.assert_called_once()
|
hud/otel/tests/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Tests for OpenTelemetry integration."""
|