Skip to content

Commit 5bb5168

Browse files
committed
fix: restore test cases after batched upload truncation
1 parent 9015b2b commit 5bb5168

2 files changed

Lines changed: 208 additions & 89 deletions

File tree

deepeval/test_run/test_run.py

Lines changed: 101 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -862,108 +862,121 @@ def post_test_run(self, test_run: TestRun) -> Optional[Tuple[str, str]]:
862862
"Sending a large test run to Confident, this might take a bit longer than usual..."
863863
)
864864

865-
####################
866-
### POST REQUEST ###
867-
####################
868-
if is_conversational_run:
869-
test_run.conversational_test_cases = initial_batch
870-
else:
871-
test_run.test_cases = initial_batch
865+
original_test_cases = test_run.test_cases
866+
original_conversational_test_cases = test_run.conversational_test_cases
867+
original_prompts = test_run.prompts
872868

873869
try:
874-
test_run.prompts = None
875-
body = test_run.model_dump(by_alias=True, exclude_none=True)
876-
except AttributeError:
877-
# Pydantic version below 2.0
878-
body = test_run.dict(by_alias=True, exclude_none=True)
870+
####################
871+
### POST REQUEST ###
872+
####################
873+
if is_conversational_run:
874+
test_run.conversational_test_cases = initial_batch
875+
else:
876+
test_run.test_cases = initial_batch
879877

880-
json_str = json.dumps(body, cls=TestRunEncoder)
881-
body = json.loads(json_str)
878+
try:
879+
test_run.prompts = None
880+
body = test_run.model_dump(by_alias=True, exclude_none=True)
881+
except AttributeError:
882+
# Pydantic version below 2.0
883+
body = test_run.dict(by_alias=True, exclude_none=True)
882884

883-
data, link = api.send_request(
884-
method=HttpMethods.POST,
885-
endpoint=Endpoints.TEST_RUN_ENDPOINT,
886-
body=body,
887-
)
885+
json_str = json.dumps(body, cls=TestRunEncoder)
886+
body = json.loads(json_str)
888887

889-
if not isinstance(data, dict) or "id" not in data:
890-
# try to show helpful details
891-
detail = None
892-
if isinstance(data, dict):
893-
detail = (
894-
data.get("detail")
895-
or data.get("message")
896-
or data.get("error")
897-
)
898-
# fall back to repr for visibility
899-
raise RuntimeError(
900-
f"Confident API response missing 'id'. "
901-
f"detail={detail!r} raw={type(data).__name__}:{repr(data)[:500]}"
888+
data, link = api.send_request(
889+
method=HttpMethods.POST,
890+
endpoint=Endpoints.TEST_RUN_ENDPOINT,
891+
body=body,
902892
)
903893

904-
res = TestRunHttpResponse(
905-
id=data["id"],
906-
)
894+
if not isinstance(data, dict) or "id" not in data:
895+
# try to show helpful details
896+
detail = None
897+
if isinstance(data, dict):
898+
detail = (
899+
data.get("detail")
900+
or data.get("message")
901+
or data.get("error")
902+
)
903+
# fall back to repr for visibility
904+
raise RuntimeError(
905+
f"Confident API response missing 'id'. "
906+
f"detail={detail!r} raw={type(data).__name__}:{repr(data)[:500]}"
907+
)
907908

908-
################################################
909-
### Send the remaining test cases in batches ###
910-
################################################
911-
total_remaining = len(remaining_test_cases_to_process)
912-
num_remaining_batches = (
913-
(total_remaining + BATCH_SIZE - 1) // BATCH_SIZE
914-
if total_remaining > 0
915-
else 0
916-
)
909+
res = TestRunHttpResponse(
910+
id=data["id"],
911+
)
917912

918-
for i in range(num_remaining_batches):
919-
start_index = i * BATCH_SIZE
920-
batch = remaining_test_cases_to_process[
921-
start_index : start_index + BATCH_SIZE
922-
]
913+
################################################
914+
### Send the remaining test cases in batches ###
915+
################################################
916+
total_remaining = len(remaining_test_cases_to_process)
917+
num_remaining_batches = (
918+
(total_remaining + BATCH_SIZE - 1) // BATCH_SIZE
919+
if total_remaining > 0
920+
else 0
921+
)
923922

924-
if len(batch) == 0:
925-
break # Should not happen with correct num_remaining_batches, but as a safeguard
923+
for i in range(num_remaining_batches):
924+
start_index = i * BATCH_SIZE
925+
batch = remaining_test_cases_to_process[
926+
start_index : start_index + BATCH_SIZE
927+
]
926928

927-
# Create RemainingTestRun with the correct list populated
928-
if is_conversational_run:
929-
remaining_test_run = RemainingTestRun(
930-
testRunId=res.id,
931-
testCases=[], # This will be empty
932-
conversationalTestCases=batch,
933-
)
934-
else:
935-
remaining_test_run = RemainingTestRun(
936-
testRunId=res.id,
937-
testCases=batch,
938-
conversationalTestCases=[], # This will be empty
939-
)
929+
if len(batch) == 0:
930+
break # Should not happen with correct num_remaining_batches, but as a safeguard
940931

941-
body = None
942-
try:
943-
body = remaining_test_run.model_dump(
944-
by_alias=True, exclude_none=True
945-
)
946-
except AttributeError:
947-
# Pydantic version below 2.0
948-
body = remaining_test_run.dict(by_alias=True, exclude_none=True)
932+
# Create RemainingTestRun with the correct list populated
933+
if is_conversational_run:
934+
remaining_test_run = RemainingTestRun(
935+
testRunId=res.id,
936+
testCases=[], # This will be empty
937+
conversationalTestCases=batch,
938+
)
939+
else:
940+
remaining_test_run = RemainingTestRun(
941+
testRunId=res.id,
942+
testCases=batch,
943+
conversationalTestCases=[], # This will be empty
944+
)
949945

950-
try:
951-
_, _ = api.send_request(
952-
method=HttpMethods.PUT,
953-
endpoint=Endpoints.TEST_RUN_ENDPOINT,
954-
body=body,
955-
)
956-
except Exception as e:
957-
message = f"Unexpected error when sending some test cases. Incomplete test run available at {link}"
958-
raise Exception(message) from e
946+
body = None
947+
try:
948+
body = remaining_test_run.model_dump(
949+
by_alias=True, exclude_none=True
950+
)
951+
except AttributeError:
952+
# Pydantic version below 2.0
953+
body = remaining_test_run.dict(
954+
by_alias=True, exclude_none=True
955+
)
959956

960-
console.print(
961-
"[rgb(5,245,141)]✓[/rgb(5,245,141)] Done 🎉! View results on "
962-
f"[link={link}]{link}[/link]"
963-
)
964-
self.save_final_test_run_link(link)
965-
open_browser(link)
966-
return link, res.id
957+
try:
958+
_, _ = api.send_request(
959+
method=HttpMethods.PUT,
960+
endpoint=Endpoints.TEST_RUN_ENDPOINT,
961+
body=body,
962+
)
963+
except Exception as e:
964+
message = f"Unexpected error when sending some test cases. Incomplete test run available at {link}"
965+
raise Exception(message) from e
966+
967+
console.print(
968+
"[rgb(5,245,141)]✓[/rgb(5,245,141)] Done 🎉! View results on "
969+
f"[link={link}]{link}[/link]"
970+
)
971+
self.save_final_test_run_link(link)
972+
open_browser(link)
973+
return link, res.id
974+
finally:
975+
test_run.test_cases = original_test_cases
976+
test_run.conversational_test_cases = (
977+
original_conversational_test_cases
978+
)
979+
test_run.prompts = original_prompts
967980

968981
def save_test_run_locally(self):
969982
local_folder = os.getenv("DEEPEVAL_RESULTS_FOLDER")

tests/test_core/test_run/test_run_manager.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import os
22
import portalocker
3+
import pytest
34

45
import deepeval.test_run.test_run as tr_mod
56

67
from types import SimpleNamespace
78

89
from deepeval.test_case import LLMTestCase
9-
from deepeval.test_run.test_run import TestRunManager, LLMApiTestCase
10+
from deepeval.test_run.test_run import (
11+
LLMApiTestCase,
12+
PromptData,
13+
TestRun,
14+
TestRunManager,
15+
)
1016
from tests.test_core.helpers import _make_fake_portalocker
1117
from tests.test_core.stubs import RecordingPortalockerLock
1218

@@ -150,3 +156,103 @@ def fake_fsync(fd: int) -> None:
150156
fsync_calls
151157
), "save_test_run(..., save_under_key=...) should call os.fsync(file.fileno())"
152158
assert fsync_calls[-1] == f.fileno()
159+
160+
161+
def _make_api_test_cases(count: int):
162+
return [
163+
LLMApiTestCase(
164+
name=f"tc{i}",
165+
input=f"in-{i}",
166+
actual_output=f"out-{i}",
167+
order=i,
168+
)
169+
for i in range(count)
170+
]
171+
172+
173+
def test_post_test_run_restores_full_test_case_list_after_batched_upload(
174+
monkeypatch,
175+
):
176+
trm = TestRunManager()
177+
test_cases = _make_api_test_cases(45)
178+
prompts = [PromptData(alias="prompt-1")]
179+
test_run = TestRun(testCases=test_cases, prompts=prompts)
180+
original_test_cases = test_run.test_cases
181+
original_prompts = test_run.prompts
182+
sent_batches = []
183+
184+
class FakeApi:
185+
def send_request(self, method, endpoint, body):
186+
sent_batches.append(
187+
(
188+
method,
189+
len(body["testCases"]),
190+
len(body["conversationalTestCases"]),
191+
)
192+
)
193+
if method == tr_mod.HttpMethods.POST:
194+
return {"id": "run-id"}, "https://confident.example/run-id"
195+
return {"ok": True}, None
196+
197+
monkeypatch.setattr(tr_mod, "Api", FakeApi)
198+
monkeypatch.setattr(tr_mod, "open_browser", lambda link: None)
199+
monkeypatch.setattr(tr_mod.console, "print", lambda *args, **kwargs: None)
200+
monkeypatch.setattr(trm, "save_final_test_run_link", lambda link: None)
201+
202+
result = trm.post_test_run(test_run)
203+
204+
assert result == ("https://confident.example/run-id", "run-id")
205+
assert test_run.test_cases is original_test_cases
206+
assert len(test_run.test_cases) == 45
207+
assert test_run.test_cases[0].name == "tc0"
208+
assert test_run.test_cases[-1].name == "tc44"
209+
assert test_run.prompts is original_prompts
210+
assert sent_batches == [
211+
(tr_mod.HttpMethods.POST, 40, 0),
212+
(tr_mod.HttpMethods.PUT, 5, 0),
213+
]
214+
215+
216+
def test_post_test_run_restores_full_test_case_list_when_batch_upload_fails(
217+
monkeypatch,
218+
):
219+
trm = TestRunManager()
220+
test_cases = _make_api_test_cases(45)
221+
prompts = [PromptData(alias="prompt-1")]
222+
test_run = TestRun(testCases=test_cases, prompts=prompts)
223+
original_test_cases = test_run.test_cases
224+
original_prompts = test_run.prompts
225+
sent_batches = []
226+
227+
class FakeApi:
228+
def send_request(self, method, endpoint, body):
229+
sent_batches.append(
230+
(
231+
method,
232+
len(body["testCases"]),
233+
len(body["conversationalTestCases"]),
234+
)
235+
)
236+
if method == tr_mod.HttpMethods.POST:
237+
return {"id": "run-id"}, "https://confident.example/run-id"
238+
raise RuntimeError("upload failed")
239+
240+
monkeypatch.setattr(tr_mod, "Api", FakeApi)
241+
monkeypatch.setattr(tr_mod, "open_browser", lambda link: None)
242+
monkeypatch.setattr(tr_mod.console, "print", lambda *args, **kwargs: None)
243+
monkeypatch.setattr(trm, "save_final_test_run_link", lambda link: None)
244+
245+
with pytest.raises(
246+
Exception, match="Unexpected error when sending some test cases"
247+
):
248+
trm.post_test_run(test_run)
249+
250+
assert test_run.test_cases is original_test_cases
251+
assert len(test_run.test_cases) == 45
252+
assert test_run.test_cases[0].name == "tc0"
253+
assert test_run.test_cases[-1].name == "tc44"
254+
assert test_run.prompts is original_prompts
255+
assert sent_batches == [
256+
(tr_mod.HttpMethods.POST, 40, 0),
257+
(tr_mod.HttpMethods.PUT, 5, 0),
258+
]

0 commit comments

Comments
 (0)