Skip to content

Commit c79105d

Browse files
madiatorclaude
andcommitted
Add unit tests for CuratorResponse and misc to meet 80% coverage
- Test all dataclass add/append methods on TokenUsage, CostInfo, RequestStats, PerformanceStats - Test CuratorResponse save/load, to_dict, get_failed_requests, __getattr__ deprecation, update_tracker_stats offline path - Test safe_model_dump normal and fallback paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 51b87d5 commit c79105d

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import json
2+
import tempfile
3+
import warnings
4+
from pathlib import Path
5+
6+
import pytest
7+
from datasets import Dataset
8+
9+
from bespokelabs.curator.types.curator_response import (
10+
CostInfo,
11+
CuratorResponse,
12+
PerformanceStats,
13+
RequestStats,
14+
TokenUsage,
15+
)
16+
17+
18+
def test_token_usage_add():
19+
a = TokenUsage(input=10, output=20, total=30)
20+
b = TokenUsage(input=5, output=15, total=20)
21+
a.add(b)
22+
assert a.input == 15
23+
assert a.output == 35
24+
assert a.total == 50
25+
26+
27+
def test_cost_info_add():
28+
a = CostInfo(total_cost=1.0, input_cost_per_million=2.0, output_cost_per_million=3.0, projected_remaining_cost=0.5)
29+
b = CostInfo(total_cost=0.5, input_cost_per_million=1.0, output_cost_per_million=1.5, projected_remaining_cost=0.2)
30+
a.add(b)
31+
assert a.total_cost == 1.5
32+
assert a.input_cost_per_million == 3.0
33+
assert a.output_cost_per_million == 4.5
34+
assert a.projected_remaining_cost == 0.7
35+
36+
37+
def test_cost_info_add_none_costs():
38+
a = CostInfo(total_cost=1.0, input_cost_per_million=None, output_cost_per_million=None)
39+
b = CostInfo(total_cost=0.5, input_cost_per_million=1.0, output_cost_per_million=1.5)
40+
a.add(b)
41+
assert a.total_cost == 1.5
42+
assert a.input_cost_per_million is None
43+
assert a.output_cost_per_million is None
44+
45+
46+
def test_request_stats_add():
47+
a = RequestStats(total=10, succeeded=8, failed=1, in_progress=1, cached=2)
48+
b = RequestStats(total=5, succeeded=4, failed=0, in_progress=1, cached=1)
49+
a.add(b)
50+
assert a.total == 15
51+
assert a.succeeded == 12
52+
assert a.failed == 1
53+
assert a.in_progress == 2
54+
assert a.cached == 3
55+
56+
57+
def test_performance_stats_add():
58+
a = PerformanceStats(total_time=10.0, requests_per_minute=60.0, input_tokens_per_minute=1000.0, output_tokens_per_minute=500.0, max_concurrent_requests=5)
59+
b = PerformanceStats(total_time=5.0, requests_per_minute=30.0, input_tokens_per_minute=500.0, output_tokens_per_minute=250.0, max_concurrent_requests=3)
60+
a.add(b)
61+
assert a.total_time == 15.0
62+
assert a.requests_per_minute == 90.0
63+
assert a.max_concurrent_requests == 8
64+
65+
66+
def test_curator_response_post_init_none_metadata():
67+
ds = Dataset.from_dict({"a": [1]})
68+
resp = CuratorResponse(dataset=ds, metadata=None)
69+
assert resp.metadata == {}
70+
71+
72+
def test_curator_response_getattr_raises():
73+
ds = Dataset.from_dict({"a": [1]})
74+
resp = CuratorResponse(dataset=ds)
75+
with warnings.catch_warnings():
76+
warnings.simplefilter("ignore", DeprecationWarning)
77+
with pytest.raises(AttributeError):
78+
_ = resp.nonexistent_attr
79+
80+
81+
def test_curator_response_update_tracker_stats_offline():
82+
ds = Dataset.from_dict({"a": [1]})
83+
resp = CuratorResponse(dataset=ds)
84+
resp.update_tracker_stats(None)
85+
assert resp.token_usage.total == 0
86+
87+
88+
def test_curator_response_get_failed_requests_no_path():
89+
ds = Dataset.from_dict({"a": [1]})
90+
resp = CuratorResponse(dataset=ds, failed_requests_path=None)
91+
assert list(resp.get_failed_requests()) == []
92+
93+
94+
def test_curator_response_get_failed_requests_with_file():
95+
ds = Dataset.from_dict({"a": [1]})
96+
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
97+
f.write(json.dumps({"error": "timeout"}) + "\n")
98+
f.write(json.dumps({"error": "rate_limit"}) + "\n")
99+
path = Path(f.name)
100+
101+
resp = CuratorResponse(dataset=ds, failed_requests_path=path)
102+
failed = list(resp.get_failed_requests())
103+
assert len(failed) == 2
104+
assert failed[0]["error"] == "timeout"
105+
path.unlink()
106+
107+
108+
def test_curator_response_append():
109+
ds = Dataset.from_dict({"a": [1]})
110+
resp1 = CuratorResponse(
111+
dataset=ds,
112+
model_name="gpt-4",
113+
token_usage=TokenUsage(input=100, output=200, total=300),
114+
cost_info=CostInfo(total_cost=1.0),
115+
request_stats=RequestStats(total=10, succeeded=9, failed=1),
116+
performance_stats=PerformanceStats(total_time=5.0),
117+
)
118+
resp2 = CuratorResponse(
119+
dataset=ds,
120+
model_name="gpt-4",
121+
token_usage=TokenUsage(input=50, output=100, total=150),
122+
cost_info=CostInfo(total_cost=0.5),
123+
request_stats=RequestStats(total=5, succeeded=5, failed=0),
124+
performance_stats=PerformanceStats(total_time=3.0),
125+
)
126+
resp1.append(resp2)
127+
assert resp1.token_usage.total == 450
128+
assert resp1.cost_info.total_cost == 1.5
129+
assert resp1.request_stats.total == 15
130+
assert resp1.performance_stats.total_time == 8.0
131+
132+
133+
def test_curator_response_to_dict():
134+
ds = Dataset.from_dict({"a": [1, 2]})
135+
resp = CuratorResponse(dataset=ds, model_name="gpt-4")
136+
d = resp.to_dict()
137+
assert d["dataset"]["size"] == 2
138+
assert d["model_name"] == "gpt-4"
139+
assert "token_usage" in d
140+
assert "cost_info" in d
141+
142+
143+
def test_curator_response_save_and_load():
144+
ds = Dataset.from_dict({"a": [1, 2]})
145+
resp = CuratorResponse(
146+
dataset=ds,
147+
model_name="gpt-4",
148+
token_usage=TokenUsage(input=100, output=200, total=300),
149+
)
150+
with tempfile.TemporaryDirectory() as tmpdir:
151+
resp.save(tmpdir)
152+
loaded = CuratorResponse.load(tmpdir, ds)
153+
assert loaded.model_name == "gpt-4"
154+
assert loaded.token_usage.input == 100
155+
assert loaded.token_usage.total == 300

‎tests/unittests/test_misc.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from bespokelabs.curator.misc import safe_model_dump
2+
3+
4+
def test_safe_model_dump_with_pydantic():
5+
from pydantic import BaseModel
6+
7+
class MyModel(BaseModel):
8+
name: str
9+
value: int
10+
11+
model = MyModel(name="test", value=42)
12+
result = safe_model_dump(model)
13+
assert result == {"name": "test", "value": 42}
14+
15+
16+
def test_safe_model_dump_fallback():
17+
18+
class FakeModel:
19+
def __init__(self):
20+
self.name = "test"
21+
self.value = 42
22+
23+
def model_dump(self):
24+
raise TypeError("not a real pydantic model")
25+
26+
result = safe_model_dump(FakeModel())
27+
assert result["name"] == "test"
28+
assert result["value"] == 42

0 commit comments

Comments
 (0)