hud-python 0.2.2__py3-none-any.whl → 0.2.4__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.

Files changed (58) hide show
  1. hud/__init__.py +4 -3
  2. hud/adapters/claude/adapter.py +5 -14
  3. hud/adapters/common/adapter.py +3 -3
  4. hud/adapters/common/tests/__init__.py +0 -0
  5. hud/adapters/common/tests/test_adapter.py +277 -0
  6. hud/adapters/common/types.py +3 -3
  7. hud/adapters/operator/adapter.py +16 -23
  8. hud/agent/__init__.py +8 -1
  9. hud/agent/base.py +28 -28
  10. hud/agent/claude.py +69 -60
  11. hud/agent/langchain.py +32 -26
  12. hud/agent/operator.py +75 -67
  13. hud/env/__init__.py +5 -5
  14. hud/env/client.py +2 -2
  15. hud/env/docker_client.py +37 -39
  16. hud/env/environment.py +91 -66
  17. hud/env/local_docker_client.py +5 -7
  18. hud/env/remote_client.py +39 -32
  19. hud/env/remote_docker_client.py +13 -3
  20. hud/evaluators/__init__.py +2 -3
  21. hud/evaluators/base.py +4 -3
  22. hud/evaluators/inspect.py +3 -8
  23. hud/evaluators/judge.py +34 -58
  24. hud/evaluators/match.py +42 -49
  25. hud/evaluators/remote.py +13 -26
  26. hud/evaluators/tests/__init__.py +0 -0
  27. hud/evaluators/tests/test_inspect.py +12 -0
  28. hud/evaluators/tests/test_judge.py +231 -0
  29. hud/evaluators/tests/test_match.py +115 -0
  30. hud/evaluators/tests/test_remote.py +98 -0
  31. hud/exceptions.py +167 -0
  32. hud/gym.py +9 -7
  33. hud/job.py +179 -109
  34. hud/server/__init__.py +2 -2
  35. hud/server/requests.py +148 -186
  36. hud/server/tests/__init__.py +0 -0
  37. hud/server/tests/test_requests.py +275 -0
  38. hud/settings.py +3 -2
  39. hud/task.py +9 -19
  40. hud/taskset.py +44 -11
  41. hud/trajectory.py +6 -9
  42. hud/types.py +12 -9
  43. hud/utils/__init__.py +2 -2
  44. hud/utils/common.py +36 -15
  45. hud/utils/config.py +45 -30
  46. hud/utils/progress.py +34 -21
  47. hud/utils/telemetry.py +10 -11
  48. hud/utils/tests/__init__.py +0 -0
  49. hud/utils/tests/test_common.py +52 -0
  50. hud/utils/tests/test_config.py +129 -0
  51. hud/utils/tests/test_progress.py +225 -0
  52. hud/utils/tests/test_telemetry.py +37 -0
  53. hud/utils/tests/test_version.py +8 -0
  54. {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/METADATA +9 -6
  55. hud_python-0.2.4.dist-info/RECORD +62 -0
  56. hud_python-0.2.2.dist-info/RECORD +0 -46
  57. {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/WHEEL +0 -0
  58. {hud_python-0.2.2.dist-info → hud_python-0.2.4.dist-info}/licenses/LICENSE +0 -0
hud/utils/progress.py CHANGED
@@ -9,12 +9,23 @@ class StepProgressTracker:
9
9
  Tracks progress across potentially parallel async tasks based on steps completed.
10
10
  Provides estimates assuming tasks run up to max_steps_per_task.
11
11
  """
12
+
12
13
  def __init__(self, total_tasks: int, max_steps_per_task: int) -> None:
14
+ """
15
+ Initialize the StepProgressTracker.
16
+
17
+ Args:
18
+ total_tasks: The total number of tasks to track.
19
+ max_steps_per_task: The maximum number of steps per task.
20
+
21
+ Raises:
22
+ ValueError: If total_tasks or max_steps_per_task is not positive.
23
+ """
13
24
  if total_tasks <= 0:
14
25
  raise ValueError("total_tasks must be positive")
15
26
  if max_steps_per_task <= 0:
16
27
  raise ValueError("max_steps_per_task must be positive")
17
-
28
+
18
29
  self.total_tasks = total_tasks
19
30
  self.max_steps_per_task = max_steps_per_task
20
31
  self.total_potential_steps = total_tasks * max_steps_per_task
@@ -26,7 +37,7 @@ class StepProgressTracker:
26
37
  self._finished_tasks: dict[str, bool] = defaultdict(bool)
27
38
  self._tasks_started = 0
28
39
  self._tasks_finished = 0
29
-
40
+
30
41
  self.start_time: float | None = None
31
42
  self.current_total_steps = 0
32
43
 
@@ -40,8 +51,10 @@ class StepProgressTracker:
40
51
 
41
52
  def increment_step(self, task_id: str) -> None:
42
53
  # async with self._lock:
43
- if (not self._finished_tasks[task_id] and
44
- self._task_steps[task_id] < self.max_steps_per_task):
54
+ if (
55
+ not self._finished_tasks[task_id]
56
+ and self._task_steps[task_id] < self.max_steps_per_task
57
+ ):
45
58
  self._task_steps[task_id] += 1
46
59
  # Update overall progress immediately
47
60
  self._update_total_steps()
@@ -55,7 +68,7 @@ class StepProgressTracker:
55
68
  self._tasks_finished += 1
56
69
  # Update overall progress
57
70
  self._update_total_steps()
58
-
71
+
59
72
  def _update_total_steps(self) -> None:
60
73
  # This could be expensive if called extremely frequently.
61
74
  # Called after increment or finish.
@@ -68,7 +81,7 @@ class StepProgressTracker:
68
81
  # Recalculate here for safety, though _update_total_steps should keep it current
69
82
  # current_steps = sum(self._task_steps.values())
70
83
  current_steps = self.current_total_steps
71
-
84
+
72
85
  percentage = 0.0
73
86
  if self.total_potential_steps > 0:
74
87
  percentage = (current_steps / self.total_potential_steps) * 100
@@ -78,7 +91,7 @@ class StepProgressTracker:
78
91
  """Returns (rate_steps_per_minute, eta_seconds_upper_bound)."""
79
92
  # async with self._lock:
80
93
  if self.start_time is None or self._tasks_started == 0:
81
- return 0.0, None # No rate or ETA yet
94
+ return 0.0, None # No rate or ETA yet
82
95
 
83
96
  elapsed_time = time.monotonic() - self.start_time
84
97
  current_steps = self.current_total_steps
@@ -86,26 +99,26 @@ class StepProgressTracker:
86
99
  rate_sec = 0.0
87
100
  if elapsed_time > 0:
88
101
  rate_sec = current_steps / elapsed_time
89
-
90
- rate_min = rate_sec * 60 # Convert rate to steps per minute
102
+
103
+ rate_min = rate_sec * 60 # Convert rate to steps per minute
91
104
 
92
105
  eta = None
93
106
  # ETA calculation still uses rate_sec (steps/second) for time estimation in seconds
94
107
  if rate_sec > 0:
95
108
  remaining_steps = self.total_potential_steps - current_steps
96
109
  eta = remaining_steps / rate_sec if remaining_steps > 0 else 0.0
97
-
98
- return rate_min, eta # Return rate in steps/min
110
+
111
+ return rate_min, eta # Return rate in steps/min
99
112
 
100
113
  def is_finished(self) -> bool:
101
- # async with self._lock:
102
- return self._tasks_finished >= self.total_tasks
114
+ # async with self._lock:
115
+ return self._tasks_finished >= self.total_tasks
103
116
 
104
117
  def display(self, bar_length: int = 40) -> str:
105
118
  """Generates a progress string similar to tqdm."""
106
119
  current_steps, total_steps, percentage = self.get_progress()
107
- rate_min, eta = self.get_stats() # Rate is now per minute
108
-
120
+ rate_min, eta = self.get_stats() # Rate is now per minute
121
+
109
122
  # Ensure valid values for display
110
123
  current_steps = min(current_steps, total_steps)
111
124
  percentage = max(0.0, min(100.0, percentage))
@@ -120,17 +133,17 @@ class StepProgressTracker:
120
133
  elapsed_seconds = int(time.monotonic() - self.start_time)
121
134
  elapsed_str = f"{elapsed_seconds // 60}:{elapsed_seconds % 60:02d}"
122
135
  if eta is not None:
123
- eta_seconds = int(eta)
124
- eta_str = f"{eta_seconds // 60}:{eta_seconds % 60:02d}"
136
+ eta_seconds = int(eta)
137
+ eta_str = f"{eta_seconds // 60}:{eta_seconds % 60:02d}"
125
138
  elif self.is_finished():
126
- eta_str = "0:00"
127
-
139
+ eta_str = "0:00"
140
+
128
141
  # Update rate string format
129
142
  rate_str = f"{rate_min:.1f} steps/min" if rate_min > 0 else "?? steps/min"
130
-
143
+
131
144
  # Format steps - use K/M for large numbers if desired, keep simple for now
132
145
  steps_str = f"{current_steps}/{total_steps}"
133
146
 
134
147
  # tasks_str = f" {self._tasks_finished}/{self.total_tasks} tasks" # Optional tasks counter
135
-
148
+
136
149
  return f"{percentage:3.0f}%|{bar}| {steps_str} [{elapsed_str}<{eta_str}, {rate_str}]"
hud/utils/telemetry.py CHANGED
@@ -4,12 +4,11 @@ import logging
4
4
 
5
5
  logger = logging.getLogger(__name__)
6
6
 
7
- def stream(live_url: str | None = None) -> str:
7
+
8
+ def stream(live_url: str) -> str:
8
9
  """
9
10
  Display a stream in the HUD system.
10
11
  """
11
- if live_url is None:
12
- raise ValueError("live_url cannot be None")
13
12
  from IPython.display import HTML, display
14
13
 
15
14
  html_content = f"""
@@ -24,44 +23,44 @@ def stream(live_url: str | None = None) -> str:
24
23
  display(HTML(html_content))
25
24
  except Exception as e:
26
25
  logger.warning(e)
27
-
26
+
28
27
  return html_content
29
28
 
30
29
 
31
30
  def display_screenshot(base64_image: str, width: int = 960, height: int = 540) -> str:
32
31
  """
33
32
  Display a base64-encoded screenshot image.
34
-
33
+
35
34
  Args:
36
35
  base64_image: Base64-encoded image string (without the data URI prefix)
37
36
  width: Display width in pixels
38
37
  height: Display height in pixels
39
-
38
+
40
39
  Returns:
41
40
  The HTML string used to display the image
42
-
41
+
43
42
  Note:
44
43
  This function will both display the image in IPython environments
45
44
  and return the HTML string for other contexts.
46
45
  """
47
46
  from IPython.display import HTML, display
48
-
47
+
49
48
  # Ensure the base64 image doesn't already have the data URI prefix
50
49
  if base64_image.startswith("data:image"):
51
50
  img_src = base64_image
52
51
  else:
53
52
  img_src = f"data:image/png;base64,{base64_image}"
54
-
53
+
55
54
  html_content = f"""
56
55
  <div style="width: {width}px; height: {height}px; overflow: hidden; margin: 10px 0; border: 1px solid #ddd;">
57
56
  <img src="{img_src}" style="max-width: 100%; max-height: 100%;">
58
57
  </div>
59
58
  """ # noqa: E501
60
-
59
+
61
60
  # Display in IPython environments
62
61
  try:
63
62
  display(HTML(html_content))
64
63
  except Exception as e:
65
64
  logger.warning(e)
66
-
65
+
67
66
  return html_content
File without changes
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import tarfile
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import pytest
9
+
10
+ from hud.utils.common import directory_to_tar_bytes, get_gym_id
11
+
12
+ if TYPE_CHECKING:
13
+ import pytest_mock
14
+
15
+
16
+ def test_directory_to_tar_bytes(tmpdir_factory: pytest.TempdirFactory):
17
+ """Test that a directory can be converted to a tar bytes object."""
18
+ temp_dir = tmpdir_factory.mktemp("test_dir")
19
+ temp_dir_path = Path(temp_dir)
20
+
21
+ (temp_dir_path / "test.txt").write_text("test content")
22
+
23
+ nested_dir = temp_dir_path / "nested"
24
+ nested_dir.mkdir(exist_ok=True)
25
+ (nested_dir / "file.txt").write_text("nested content")
26
+
27
+ tar_bytes = directory_to_tar_bytes(temp_dir_path)
28
+ assert tar_bytes is not None
29
+ assert len(tar_bytes) > 0
30
+
31
+ with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
32
+ members = tar.getmembers()
33
+ member_names = {m.name for m in members}
34
+
35
+ assert "test.txt" in member_names
36
+ assert "nested/file.txt" in member_names
37
+
38
+ test_content = tar.extractfile("test.txt")
39
+ assert test_content is not None
40
+ assert test_content.read().decode() == "test content"
41
+
42
+ nested_content = tar.extractfile("nested/file.txt")
43
+ assert nested_content is not None
44
+ assert nested_content.read().decode() == "nested content"
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_get_gym_id(mocker: pytest_mock.MockerFixture):
49
+ """Test that the gym ID can be retrieved."""
50
+ mocker.patch("hud.utils.common.make_request", return_value={"id": "test_gym_id"})
51
+ gym_id = await get_gym_id("test_gym")
52
+ assert gym_id == "test_gym_id"
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from hud.utils.common import FunctionConfig
6
+ from hud.utils.config import (
7
+ _is_list_of_configs,
8
+ _is_valid_python_name,
9
+ _split_and_validate_path,
10
+ _validate_hud_config,
11
+ expand_config,
12
+ )
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ "config, expected",
17
+ [
18
+ ("test", [{"function": "test", "args": [], "id": None}]),
19
+ (("test",), [{"function": "test", "args": [], "id": None}]),
20
+ (
21
+ [FunctionConfig(function="test", args=[])],
22
+ [{"function": "test", "args": [], "id": None}],
23
+ ),
24
+ ({"function": "test", "args": []}, [{"function": "test", "args": [], "id": None}]),
25
+ (
26
+ {"function": "test", "args": ["arg1"]},
27
+ [{"function": "test", "args": ["arg1"], "id": None}],
28
+ ),
29
+ (
30
+ {"function": "test", "args": ["arg1"], "id": "test_id"},
31
+ [{"function": "test", "args": ["arg1"], "id": "test_id"}],
32
+ ),
33
+ (("test", "arg1", "arg2"), [{"function": "test", "args": ["arg1", "arg2"], "id": None}]),
34
+ ],
35
+ )
36
+ def test_expand_config(config, expected):
37
+ result = expand_config(config)
38
+ assert len(result) == len(expected)
39
+ for i, item in enumerate(result):
40
+ assert item.function == expected[i]["function"]
41
+ assert item.args == expected[i]["args"]
42
+ assert item.id == expected[i]["id"]
43
+
44
+
45
+ @pytest.mark.parametrize(
46
+ "name, expected",
47
+ [
48
+ ("valid_name", True),
49
+ ("ValidName", True),
50
+ ("valid_name_123", True),
51
+ ("_valid_name", True),
52
+ ("123_invalid", False),
53
+ ("invalid-name", False),
54
+ ("", False),
55
+ ],
56
+ )
57
+ def test_is_valid_python_name(name, expected):
58
+ assert _is_valid_python_name(name) == expected
59
+
60
+
61
+ def test_validate_hud_config_valid():
62
+ config = {"function": "test.func", "args": ["arg1", "arg2"]}
63
+ result = _validate_hud_config(config)
64
+ assert result.function == "test.func"
65
+ assert result.args == ["arg1", "arg2"]
66
+ assert result.id is None
67
+
68
+ # Test with single arg (not in a list)
69
+ config = {"function": "test.func", "args": "arg1"}
70
+ result = _validate_hud_config(config)
71
+ assert result.function == "test.func"
72
+ assert result.args == ["arg1"]
73
+
74
+ # Test with ID
75
+ config = {"function": "test.func", "args": [], "id": "test_id"}
76
+ result = _validate_hud_config(config)
77
+ assert result.id == "test_id"
78
+
79
+
80
+ def test_validate_hud_config_invalid():
81
+ with pytest.raises(ValueError, match="function must be a string"):
82
+ _validate_hud_config({"args": []})
83
+
84
+ with pytest.raises(ValueError, match="function must be a string"):
85
+ _validate_hud_config({"function": 123, "args": []})
86
+
87
+
88
+ def test_split_and_validate_path_valid():
89
+ # none should raise
90
+ _split_and_validate_path("module.submodule.function")
91
+ _split_and_validate_path("function")
92
+ _split_and_validate_path("Module_123.function_456")
93
+
94
+
95
+ def test_split_and_validate_path_invalid():
96
+ with pytest.raises(ValueError, match="Invalid Python identifier in path"):
97
+ _split_and_validate_path("invalid-module.function")
98
+
99
+
100
+ def test_is_list_of_configs():
101
+ valid_list = [
102
+ FunctionConfig(function="test1", args=[]),
103
+ FunctionConfig(function="test2", args=["arg1"]),
104
+ ]
105
+ assert _is_list_of_configs(valid_list) is True
106
+
107
+ # Empty list
108
+ assert _is_list_of_configs([]) is True
109
+
110
+ # Invalid: not a list
111
+ assert _is_list_of_configs("not_a_list") is False
112
+
113
+ # Invalid: list with non-FunctionConfig items
114
+ invalid_list = [FunctionConfig(function="test", args=[]), "not_a_function_config"]
115
+ assert _is_list_of_configs(invalid_list) is False
116
+
117
+
118
+ def test_expand_config_errors():
119
+ with pytest.raises(ValueError):
120
+ empty_tuple = ()
121
+ expand_config(empty_tuple) # type: ignore
122
+
123
+ with pytest.raises(ValueError):
124
+ invalid_tuple = (123, "arg1")
125
+ expand_config(invalid_tuple) # type: ignore
126
+
127
+ with pytest.raises(ValueError, match="Unknown configuration type"):
128
+ invalid_value = 123
129
+ expand_config(invalid_value) # type: ignore
@@ -0,0 +1,225 @@
1
+ """Tests for the progress tracking utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from hud.utils.progress import StepProgressTracker
8
+
9
+
10
+ @pytest.fixture
11
+ def tracker():
12
+ return StepProgressTracker(total_tasks=2, max_steps_per_task=10)
13
+
14
+
15
+ def test_invalid_inputs_init():
16
+ with pytest.raises(ValueError, match="total_tasks must be positive"):
17
+ StepProgressTracker(total_tasks=0, max_steps_per_task=10)
18
+
19
+ with pytest.raises(ValueError, match="max_steps_per_task must be positive"):
20
+ StepProgressTracker(total_tasks=5, max_steps_per_task=0)
21
+
22
+
23
+ def test_start_task(tracker):
24
+ assert tracker.start_time is None
25
+ assert tracker._tasks_started == 0
26
+
27
+ tracker.start_task("task1")
28
+
29
+ assert tracker.start_time is not None
30
+ assert tracker._tasks_started == 1
31
+ assert tracker._task_steps["task1"] == 0
32
+ assert not tracker._finished_tasks["task1"]
33
+
34
+ tracker.start_task("task2")
35
+ assert tracker._tasks_started == 2
36
+ assert tracker._task_steps["task2"] == 0
37
+ assert not tracker._finished_tasks["task2"]
38
+
39
+
40
+ def test_increment_step(tracker):
41
+ tracker.start_task("task1")
42
+ assert tracker.current_total_steps == 0
43
+
44
+ tracker.increment_step("task1")
45
+ assert tracker._task_steps["task1"] == 1
46
+ assert tracker.current_total_steps == 1
47
+
48
+ tracker.increment_step("task1")
49
+ tracker.increment_step("task1")
50
+ assert tracker._task_steps["task1"] == 3
51
+ assert tracker.current_total_steps == 3
52
+
53
+ tracker.start_task("task2")
54
+ tracker.increment_step("task2")
55
+ assert tracker._task_steps["task2"] == 1
56
+ assert tracker.current_total_steps == 4
57
+
58
+ tracker.finish_task("task1")
59
+ initial_steps = tracker.current_total_steps
60
+ tracker.increment_step("task1")
61
+ assert tracker.current_total_steps == initial_steps
62
+
63
+ for _ in range(15):
64
+ tracker.increment_step("task2")
65
+ assert tracker._task_steps["task2"] <= tracker.max_steps_per_task
66
+
67
+
68
+ def test_finish_task(tracker):
69
+ tracker.start_task("task1")
70
+ tracker.start_task("task2")
71
+
72
+ tracker.increment_step("task1")
73
+ tracker.increment_step("task1")
74
+ initial_steps = tracker._task_steps["task1"]
75
+
76
+ tracker.finish_task("task1")
77
+
78
+ assert tracker._finished_tasks["task1"]
79
+ assert tracker._tasks_finished == 1
80
+ assert tracker._task_steps["task1"] == tracker.max_steps_per_task
81
+ assert tracker.current_total_steps > initial_steps
82
+
83
+ current_steps = tracker.current_total_steps
84
+ tracker.finish_task("task1")
85
+ assert tracker._tasks_finished == 1
86
+ assert tracker.current_total_steps == current_steps
87
+
88
+
89
+ def test_get_progress(tracker):
90
+ steps, total, percentage = tracker.get_progress()
91
+ assert steps == 0
92
+ assert total == tracker.total_potential_steps
93
+ assert percentage == 0.0
94
+
95
+ tracker.start_task("task1")
96
+ tracker.increment_step("task1")
97
+ steps, total, percentage = tracker.get_progress()
98
+ assert steps == 1
99
+ assert total == tracker.total_potential_steps
100
+ assert percentage == (1 / tracker.total_potential_steps) * 100
101
+
102
+ tracker.finish_task("task1")
103
+ steps, total, percentage = tracker.get_progress()
104
+ assert steps == tracker.max_steps_per_task
105
+ assert total == tracker.total_potential_steps
106
+ assert percentage == (tracker.max_steps_per_task / tracker.total_potential_steps) * 100
107
+
108
+ tracker.start_task("task2")
109
+ tracker.finish_task("task2")
110
+ steps, total, percentage = tracker.get_progress()
111
+ assert steps == tracker.total_potential_steps
112
+ assert percentage == 100.0
113
+
114
+
115
+ def test_get_stats_no_progress(tracker, mocker):
116
+ rate, eta = tracker.get_stats()
117
+ assert rate == 0.0
118
+ assert eta is None
119
+
120
+ mocker.patch("time.monotonic", return_value=100.0)
121
+ tracker.start_task("task1")
122
+
123
+ mocker.patch("time.monotonic", return_value=100.0)
124
+ rate, eta = tracker.get_stats()
125
+ assert rate == 0.0
126
+ assert eta is None
127
+
128
+
129
+ def test_get_stats_with_progress(mocker):
130
+ mock_time = mocker.patch("time.monotonic")
131
+ mock_time.return_value = 100.0
132
+
133
+ tracker = StepProgressTracker(total_tasks=1, max_steps_per_task=10)
134
+ tracker.start_task("task1")
135
+
136
+ mock_time.return_value = 160.0
137
+ for _ in range(5):
138
+ tracker.increment_step("task1")
139
+
140
+ rate, eta = tracker.get_stats()
141
+
142
+ assert rate == pytest.approx(5.0)
143
+ assert eta == pytest.approx(60.0)
144
+
145
+ for _ in range(5):
146
+ tracker.increment_step("task1")
147
+
148
+ rate, eta = tracker.get_stats()
149
+ assert rate == pytest.approx(10.0)
150
+ assert eta == pytest.approx(0.0)
151
+
152
+
153
+ def test_is_finished(tracker):
154
+ assert not tracker.is_finished()
155
+
156
+ tracker.start_task("task1")
157
+ tracker.finish_task("task1")
158
+ assert not tracker.is_finished()
159
+
160
+ tracker.start_task("task2")
161
+ tracker.finish_task("task2")
162
+ assert tracker.is_finished()
163
+
164
+
165
+ def test_display(tracker, mocker):
166
+ mock_time = mocker.patch("time.monotonic")
167
+ mock_time.return_value = 100.0
168
+ tracker.start_task("task1")
169
+
170
+ mock_time.return_value = 130.0
171
+ tracker.increment_step("task1")
172
+ tracker.increment_step("task1")
173
+
174
+ display_str = tracker.display()
175
+
176
+ assert "%" in display_str
177
+ assert "2/20" in display_str
178
+ assert "0:30" in display_str
179
+ assert "steps/min" in display_str
180
+
181
+ tracker.finish_task("task1")
182
+ display_str = tracker.display()
183
+ assert "10/20" in display_str
184
+
185
+ tracker.start_task("task2")
186
+ tracker.finish_task("task2")
187
+ display_str = tracker.display()
188
+ assert "100%" in display_str
189
+ assert "20/20" in display_str
190
+
191
+
192
+ def test_complex_workflow():
193
+ tracker = StepProgressTracker(total_tasks=5, max_steps_per_task=20)
194
+
195
+ for i in range(5):
196
+ tracker.start_task(f"task{i}")
197
+
198
+ for _ in range(10):
199
+ tracker.increment_step("task0")
200
+
201
+ for _ in range(5):
202
+ tracker.increment_step("task1")
203
+
204
+ tracker.finish_task("task2")
205
+
206
+ for _ in range(15):
207
+ tracker.increment_step("task3")
208
+
209
+ tracker.finish_task("task3")
210
+
211
+ steps, total, percentage = tracker.get_progress()
212
+ expected_steps = 10 + 5 + 20 + 20 + 0
213
+ assert steps == expected_steps
214
+ assert total == 5 * 20
215
+ assert percentage == (expected_steps / total) * 100
216
+
217
+ assert tracker._tasks_finished == 2
218
+ assert not tracker.is_finished()
219
+
220
+ tracker.finish_task("task0")
221
+ tracker.finish_task("task1")
222
+ tracker.finish_task("task4")
223
+
224
+ assert tracker.is_finished()
225
+ assert tracker.get_progress()[2] == 100.0
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from hud.utils.telemetry import stream
4
+
5
+
6
+ def test_stream():
7
+ html_content = stream("https://example.com")
8
+ assert html_content is not None
9
+ assert "<div style=" in html_content
10
+ assert 'src="https://example.com"' in html_content
11
+
12
+
13
+ def test_display_screenshot():
14
+ from hud.utils.telemetry import display_screenshot
15
+
16
+ # This is a simple 1x1 transparent PNG image in base64 format
17
+ base64_image = (
18
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQ"
19
+ "AAABJRU5ErkJggg=="
20
+ )
21
+
22
+ html_content = display_screenshot(base64_image)
23
+ assert html_content is not None
24
+ assert "<div style=" in html_content
25
+ assert "width: 960px" in html_content
26
+ assert "height: 540px" in html_content
27
+ assert f"data:image/png;base64,{base64_image}" in html_content
28
+
29
+ # Test with custom dimensions
30
+ custom_html = display_screenshot(base64_image, width=800, height=600)
31
+ assert "width: 800px" in custom_html
32
+ assert "height: 600px" in custom_html
33
+
34
+ # Test with data URI already included
35
+ data_uri = f"data:image/png;base64,{base64_image}"
36
+ uri_html = display_screenshot(data_uri)
37
+ assert data_uri in uri_html
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def test_import():
5
+ """Test that the package can be imported."""
6
+ import hud
7
+
8
+ assert hud.__version__ == "0.2.4"