chore: import upstream snapshot with attribution
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
"""Unit tests for test_suite.converters module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pandas as pd
|
||||
import pandas.testing
|
||||
import pytest
|
||||
from unittest import mock
|
||||
|
||||
from opik.api_objects.dataset import dataset_item
|
||||
from opik.api_objects.dataset.test_suite import converters
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# evaluators_to_assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_evaluators_to_assertions__multiple_evaluators__concatenates():
|
||||
e1 = mock.MagicMock()
|
||||
e1.assertions = ["A1"]
|
||||
e2 = mock.MagicMock()
|
||||
e2.assertions = ["A2", "A3"]
|
||||
|
||||
assert converters.evaluators_to_assertions([e1, e2]) == ["A1", "A2", "A3"]
|
||||
|
||||
|
||||
def test_evaluators_to_assertions__empty_list__returns_empty():
|
||||
assert converters.evaluators_to_assertions([]) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# version_evaluators_to_assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_version_evaluators_to_assertions__llm_judge__extracts_assertions():
|
||||
from opik.evaluation.suite_evaluators import LLMJudge
|
||||
|
||||
judge = LLMJudge(assertions=["Response is accurate"], track=False)
|
||||
config = judge.to_config().model_dump(by_alias=True)
|
||||
|
||||
evaluator_item = mock.MagicMock()
|
||||
evaluator_item.type = "llm_judge"
|
||||
evaluator_item.config = config
|
||||
|
||||
assert converters.version_evaluators_to_assertions([evaluator_item]) == [
|
||||
"Response is accurate"
|
||||
]
|
||||
|
||||
|
||||
def test_version_evaluators_to_assertions__none__returns_empty():
|
||||
assert converters.version_evaluators_to_assertions(None) == []
|
||||
|
||||
|
||||
def test_version_evaluators_to_assertions__non_llm_judge__skipped():
|
||||
evaluator_item = mock.MagicMock()
|
||||
evaluator_item.type = "custom_scorer"
|
||||
|
||||
assert converters.version_evaluators_to_assertions([evaluator_item]) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# version_policy_to_execution_policy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_version_policy_to_execution_policy__converts():
|
||||
policy = mock.MagicMock()
|
||||
policy.runs_per_item = 5
|
||||
policy.pass_threshold = 3
|
||||
|
||||
assert converters.version_policy_to_execution_policy(policy) == {
|
||||
"runs_per_item": 5,
|
||||
"pass_threshold": 3,
|
||||
}
|
||||
|
||||
|
||||
def test_version_policy_to_execution_policy__none__returns_default():
|
||||
assert converters.version_policy_to_execution_policy(None) == {
|
||||
"runs_per_item": 1,
|
||||
"pass_threshold": 1,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dataset_item_to_suite_item_dict (DatasetItem → TestSuiteItem)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_dataset_item_to_suite_item_dict__all_fields():
|
||||
item = dataset_item.DatasetItem(
|
||||
id="item-1",
|
||||
description="Test item",
|
||||
question="What is 2+2?",
|
||||
execution_policy=dataset_item.ExecutionPolicyItem(
|
||||
runs_per_item=3, pass_threshold=2
|
||||
),
|
||||
)
|
||||
|
||||
assert converters.dataset_item_to_suite_item_dict(item) == {
|
||||
"id": "item-1",
|
||||
"data": {"question": "What is 2+2?"},
|
||||
"assertions": [],
|
||||
"description": "Test item",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_dataset_item_to_suite_item_dict__minimal():
|
||||
item = dataset_item.DatasetItem(id="item-1", question="Hello")
|
||||
|
||||
result = converters.dataset_item_to_suite_item_dict(item)
|
||||
|
||||
assert result == {"id": "item-1", "data": {"question": "Hello"}, "assertions": []}
|
||||
assert "description" not in result
|
||||
assert "execution_policy" not in result
|
||||
|
||||
|
||||
def test_dataset_item_to_suite_item_dict__with_evaluators__extracts_assertions():
|
||||
from opik.evaluation.suite_evaluators import LLMJudge
|
||||
|
||||
judge = LLMJudge(assertions=["Is correct"], track=False)
|
||||
config = judge.to_config().model_dump(by_alias=True)
|
||||
|
||||
item = dataset_item.DatasetItem(
|
||||
id="item-2",
|
||||
evaluators=[
|
||||
dataset_item.EvaluatorItem(
|
||||
name="llm_judge", type="llm_judge", config=config
|
||||
),
|
||||
],
|
||||
question="Hello",
|
||||
)
|
||||
|
||||
assert converters.dataset_item_to_suite_item_dict(item)["assertions"] == [
|
||||
"Is correct"
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# suite_item_dict_to_dataset_item (TestSuiteItem → DatasetItem)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_suite_item_dict_to_dataset_item__all_fields():
|
||||
item = {
|
||||
"id": "item-1",
|
||||
"data": {"question": "Hello", "context": "test"},
|
||||
"assertions": ["Is polite"],
|
||||
"description": "A test case",
|
||||
"execution_policy": {"runs_per_item": 5, "pass_threshold": 3},
|
||||
}
|
||||
|
||||
ds_item = converters.suite_item_dict_to_dataset_item(item)
|
||||
|
||||
assert ds_item.id == "item-1"
|
||||
assert ds_item.get_content() == {"question": "Hello", "context": "test"}
|
||||
assert ds_item.description == "A test case"
|
||||
assert ds_item.execution_policy is not None
|
||||
assert ds_item.execution_policy.runs_per_item == 5
|
||||
assert ds_item.execution_policy.pass_threshold == 3
|
||||
assert ds_item.evaluators is not None
|
||||
assert len(ds_item.evaluators) == 1
|
||||
assert ds_item.evaluators[0].type == "llm_judge"
|
||||
|
||||
|
||||
def test_suite_item_dict_to_dataset_item__minimal__generates_id():
|
||||
ds_item = converters.suite_item_dict_to_dataset_item(
|
||||
{"data": {"question": "Hello"}}
|
||||
)
|
||||
|
||||
assert ds_item.id is not None
|
||||
assert len(ds_item.id) > 0
|
||||
assert ds_item.get_content() == {"question": "Hello"}
|
||||
assert ds_item.evaluators is None
|
||||
assert ds_item.execution_policy is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# to_json / from_json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
SAMPLE_ITEMS = [
|
||||
{
|
||||
"id": "item-1",
|
||||
"data": {"question": "How do I get a refund?", "context": "Premium user"},
|
||||
"assertions": ["Response is polite"],
|
||||
"description": "Refund scenario",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
},
|
||||
{
|
||||
"id": "item-2",
|
||||
"data": {"question": "Is my account hacked?"},
|
||||
"assertions": [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_to_json__happyflow():
|
||||
EXPECTED = [
|
||||
{
|
||||
"id": "item-1",
|
||||
"data": {"question": "How do I get a refund?", "context": "Premium user"},
|
||||
"assertions": ["Response is polite"],
|
||||
"description": "Refund scenario",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
},
|
||||
{
|
||||
"id": "item-2",
|
||||
"data": {"question": "Is my account hacked?"},
|
||||
"assertions": [],
|
||||
},
|
||||
]
|
||||
|
||||
assert json.loads(converters.to_json(SAMPLE_ITEMS)) == EXPECTED
|
||||
|
||||
|
||||
def test_to_json__empty_list():
|
||||
assert json.loads(converters.to_json([])) == []
|
||||
|
||||
|
||||
def test_from_json__happyflow():
|
||||
json_str = json.dumps(
|
||||
[
|
||||
{"data": {"question": "Hello"}, "assertions": ["Is polite"]},
|
||||
{"data": {"question": "Bye"}},
|
||||
]
|
||||
)
|
||||
|
||||
EXPECTED = [
|
||||
{"data": {"question": "Hello"}, "assertions": ["Is polite"]},
|
||||
{"data": {"question": "Bye"}},
|
||||
]
|
||||
|
||||
assert converters.from_json(json_str, {}, []) == EXPECTED
|
||||
|
||||
|
||||
def test_from_json__with_keys_mapping():
|
||||
json_str = json.dumps(
|
||||
[
|
||||
{"test_data": {"question": "Hello"}, "checks": ["Is polite"]},
|
||||
]
|
||||
)
|
||||
|
||||
EXPECTED = [{"data": {"question": "Hello"}, "assertions": ["Is polite"]}]
|
||||
|
||||
assert (
|
||||
converters.from_json(
|
||||
json_str, {"test_data": "data", "checks": "assertions"}, []
|
||||
)
|
||||
== EXPECTED
|
||||
)
|
||||
|
||||
|
||||
def test_from_json__with_ignore_keys():
|
||||
json_str = json.dumps(
|
||||
[
|
||||
{"data": {"question": "Hello"}, "internal_note": "skip this"},
|
||||
]
|
||||
)
|
||||
|
||||
EXPECTED = [{"data": {"question": "Hello"}}]
|
||||
|
||||
assert converters.from_json(json_str, {}, ["internal_note"]) == EXPECTED
|
||||
|
||||
|
||||
def test_from_json__non_array__raises_value_error():
|
||||
json_str = json.dumps({"data": {"question": "Hello"}})
|
||||
|
||||
with pytest.raises(ValueError, match="must be an array"):
|
||||
converters.from_json(json_str, {}, [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# to_pandas / from_pandas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_to_pandas__happyflow():
|
||||
EXPECTED = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"id": "item-1",
|
||||
"data": {
|
||||
"question": "How do I get a refund?",
|
||||
"context": "Premium user",
|
||||
},
|
||||
"assertions": ["Response is polite"],
|
||||
"description": "Refund scenario",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
},
|
||||
{
|
||||
"id": "item-2",
|
||||
"data": {"question": "Is my account hacked?"},
|
||||
"assertions": [],
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
pandas.testing.assert_frame_equal(converters.to_pandas(SAMPLE_ITEMS), EXPECTED)
|
||||
|
||||
|
||||
def test_to_pandas__empty_list():
|
||||
assert len(converters.to_pandas([])) == 0
|
||||
|
||||
|
||||
def test_from_pandas__happyflow():
|
||||
dataframe = pd.DataFrame(
|
||||
[
|
||||
{"data": {"question": "Hello"}, "assertions": ["Is polite"]},
|
||||
{"data": {"question": "Bye"}},
|
||||
]
|
||||
)
|
||||
|
||||
EXPECTED = [
|
||||
{"data": {"question": "Hello"}, "assertions": ["Is polite"]},
|
||||
{"data": {"question": "Bye"}},
|
||||
]
|
||||
|
||||
assert converters.from_pandas(dataframe, {}, []) == EXPECTED
|
||||
|
||||
|
||||
def test_from_pandas__with_keys_mapping():
|
||||
dataframe = pd.DataFrame([{"test_data": {"question": "Hello"}}])
|
||||
|
||||
EXPECTED = [{"data": {"question": "Hello"}}]
|
||||
|
||||
assert converters.from_pandas(dataframe, {"test_data": "data"}, []) == EXPECTED
|
||||
|
||||
|
||||
def test_from_pandas__nan_values__skipped():
|
||||
dataframe = pd.DataFrame(
|
||||
[
|
||||
{"data": {"question": "Hello"}, "assertions": ["Is polite"]},
|
||||
{"data": {"question": "Bye"}, "assertions": float("nan")},
|
||||
]
|
||||
)
|
||||
|
||||
result = converters.from_pandas(dataframe, {}, [])
|
||||
|
||||
assert result[0] == {"data": {"question": "Hello"}, "assertions": ["Is polite"]}
|
||||
assert result[1] == {"data": {"question": "Bye"}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# from_jsonl_file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_from_jsonl_file__happyflow():
|
||||
jsonl_content = (
|
||||
'{"data": {"question": "What is 2+2?"}, "assertions": ["Is correct"]}\n'
|
||||
'{"data": {"question": "Capital of France?"}}\n'
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||||
f.write(jsonl_content)
|
||||
path = f.name
|
||||
|
||||
try:
|
||||
EXPECTED = [
|
||||
{"data": {"question": "What is 2+2?"}, "assertions": ["Is correct"]},
|
||||
{"data": {"question": "Capital of France?"}},
|
||||
]
|
||||
|
||||
assert converters.from_jsonl_file(path, {}, []) == EXPECTED
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_from_jsonl_file__empty_lines__skipped():
|
||||
jsonl_content = '{"data": {"question": "Q1"}}\n\n{"data": {"question": "Q2"}}\n\n'
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||||
f.write(jsonl_content)
|
||||
path = f.name
|
||||
|
||||
try:
|
||||
assert len(converters.from_jsonl_file(path, {}, [])) == 2
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_from_jsonl_file__with_keys_mapping():
|
||||
jsonl_content = '{"test_data": {"question": "Hello"}}\n'
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||||
f.write(jsonl_content)
|
||||
path = f.name
|
||||
|
||||
try:
|
||||
EXPECTED = [{"data": {"question": "Hello"}}]
|
||||
|
||||
assert converters.from_jsonl_file(path, {"test_data": "data"}, []) == EXPECTED
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Round-trip tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_adapter_roundtrip__suite_to_dataset_to_suite__preserves_all_fields():
|
||||
original = {
|
||||
"id": "item-1",
|
||||
"data": {"question": "Hello", "context": "Premium"},
|
||||
"assertions": ["Is polite", "Is helpful"],
|
||||
"description": "Test case",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
}
|
||||
|
||||
ds_item = converters.suite_item_dict_to_dataset_item(original)
|
||||
recovered = converters.dataset_item_to_suite_item_dict(ds_item)
|
||||
|
||||
assert recovered["id"] == original["id"]
|
||||
assert recovered["data"] == original["data"]
|
||||
assert sorted(recovered["assertions"]) == sorted(original["assertions"])
|
||||
assert recovered["description"] == original["description"]
|
||||
assert recovered["execution_policy"] == original["execution_policy"]
|
||||
|
||||
|
||||
def test_adapter_roundtrip__two_cycles__stable():
|
||||
original = {
|
||||
"id": "item-1",
|
||||
"data": {"question": "Hello"},
|
||||
"assertions": ["Is polite"],
|
||||
"execution_policy": {"runs_per_item": 5, "pass_threshold": 3},
|
||||
}
|
||||
|
||||
suite_item = original
|
||||
for _ in range(2):
|
||||
ds_item = converters.suite_item_dict_to_dataset_item(suite_item)
|
||||
suite_item = converters.dataset_item_to_suite_item_dict(ds_item)
|
||||
|
||||
assert suite_item["data"] == original["data"]
|
||||
assert suite_item["assertions"] == original["assertions"]
|
||||
assert suite_item["execution_policy"] == original["execution_policy"]
|
||||
|
||||
|
||||
def test_json_roundtrip__export_import_export__stable():
|
||||
json_str_1 = converters.to_json(SAMPLE_ITEMS)
|
||||
imported = converters.from_json(json_str_1, {}, [])
|
||||
json_str_2 = converters.to_json(imported)
|
||||
|
||||
assert json.loads(json_str_1) == json.loads(json_str_2)
|
||||
|
||||
|
||||
def test_json_roundtrip__import_export_import__stable():
|
||||
json_str = json.dumps(
|
||||
[
|
||||
{
|
||||
"data": {"question": "Hello"},
|
||||
"assertions": ["Is polite"],
|
||||
"description": "Test",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
items_1 = converters.from_json(json_str, {}, [])
|
||||
exported = converters.to_json(items_1)
|
||||
items_2 = converters.from_json(exported, {}, [])
|
||||
|
||||
assert items_1 == items_2
|
||||
|
||||
|
||||
def test_pandas_roundtrip__export_import_export__stable():
|
||||
df_1 = converters.to_pandas(SAMPLE_ITEMS)
|
||||
imported = converters.from_pandas(df_1, {}, [])
|
||||
df_2 = converters.to_pandas(imported)
|
||||
|
||||
pandas.testing.assert_frame_equal(df_1, df_2)
|
||||
|
||||
|
||||
def test_full_roundtrip__json_through_adapters__two_cycles():
|
||||
"""JSON → from_json → adapter → adapter → to_json, repeated twice."""
|
||||
original_json = json.dumps(
|
||||
[
|
||||
{
|
||||
"id": "item-1",
|
||||
"data": {"question": "Refund?", "tier": "premium"},
|
||||
"assertions": ["Is polite", "No hallucination"],
|
||||
"description": "Refund scenario",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
current_json = original_json
|
||||
for _ in range(2):
|
||||
suite_items = converters.from_json(current_json, {}, [])
|
||||
ds_items = [converters.suite_item_dict_to_dataset_item(i) for i in suite_items]
|
||||
suite_items = [converters.dataset_item_to_suite_item_dict(i) for i in ds_items]
|
||||
current_json = converters.to_json(suite_items)
|
||||
|
||||
EXPECTED = {
|
||||
"id": "item-1",
|
||||
"data": {"question": "Refund?", "tier": "premium"},
|
||||
"assertions": ["Is polite", "No hallucination"],
|
||||
"description": "Refund scenario",
|
||||
"execution_policy": {"runs_per_item": 3, "pass_threshold": 2},
|
||||
}
|
||||
|
||||
final = json.loads(current_json)
|
||||
assert len(final) == 1
|
||||
result = final[0]
|
||||
assert result["id"] == EXPECTED["id"]
|
||||
assert result["data"] == EXPECTED["data"]
|
||||
assert sorted(result["assertions"]) == sorted(EXPECTED["assertions"])
|
||||
assert result["description"] == EXPECTED["description"]
|
||||
assert result["execution_policy"] == EXPECTED["execution_policy"]
|
||||
@@ -0,0 +1,317 @@
|
||||
"""Unit tests for test suite result file generation."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
from opik.api_objects.dataset.test_suite import (
|
||||
suite_result_constructor,
|
||||
types as suite_types,
|
||||
)
|
||||
from opik.api_objects.dataset.test_suite.report_processors import file_writer
|
||||
from opik.api_objects.dataset import dataset_item
|
||||
from opik.evaluation import evaluation_result, test_result, test_case
|
||||
from opik.evaluation.metrics import score_result
|
||||
|
||||
|
||||
def _make_test_result(
|
||||
dataset_item_id: str,
|
||||
trial_id: int,
|
||||
scores: list[tuple[str, float]],
|
||||
task_output: dict | None = None,
|
||||
dataset_item_content: dict | None = None,
|
||||
execution_policy: dict | None = None,
|
||||
task_execution_time: float | None = None,
|
||||
scoring_time: float | None = None,
|
||||
) -> test_result.TestResult:
|
||||
ds_item = None
|
||||
if execution_policy is not None:
|
||||
ds_item = dataset_item.DatasetItem(
|
||||
id=dataset_item_id,
|
||||
execution_policy=dataset_item.ExecutionPolicyItem(
|
||||
runs_per_item=execution_policy.get("runs_per_item"),
|
||||
pass_threshold=execution_policy.get("pass_threshold"),
|
||||
),
|
||||
)
|
||||
|
||||
return test_result.TestResult(
|
||||
test_case=test_case.TestCase(
|
||||
trace_id=f"trace-{dataset_item_id}-{trial_id}",
|
||||
dataset_item_id=dataset_item_id,
|
||||
task_output=task_output or {"input": "test", "output": "result"},
|
||||
dataset_item_content=dataset_item_content or {"question": "What?"},
|
||||
dataset_item=ds_item,
|
||||
),
|
||||
score_results=[
|
||||
score_result.ScoreResult(name=name, value=value) for name, value in scores
|
||||
],
|
||||
trial_id=trial_id,
|
||||
task_execution_time=task_execution_time,
|
||||
scoring_time=scoring_time,
|
||||
)
|
||||
|
||||
|
||||
def _make_suite_result(
|
||||
test_results_list: list[test_result.TestResult],
|
||||
suite_name: str | None = None,
|
||||
total_time: float | None = None,
|
||||
) -> suite_types.TestSuiteResult:
|
||||
eval_result = evaluation_result.EvaluationResult(
|
||||
experiment_id="exp-123",
|
||||
dataset_id="dataset-456",
|
||||
experiment_name="my-experiment",
|
||||
test_results=test_results_list,
|
||||
experiment_url="http://example.com/experiment/exp-123",
|
||||
trial_count=1,
|
||||
)
|
||||
return suite_result_constructor.build_suite_result(
|
||||
eval_result,
|
||||
suite_name=suite_name,
|
||||
total_time=total_time,
|
||||
)
|
||||
|
||||
|
||||
class TestToReportDict:
|
||||
def test_to_report_dict__single_item_with_mixed_scores__returns_correct_structure(
|
||||
self,
|
||||
):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("Is polite", True), ("Is helpful", False)],
|
||||
task_output={"input": "hi", "output": "hello"},
|
||||
dataset_item_content={"question": "hi"},
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
task_execution_time=1.234,
|
||||
scoring_time=0.567,
|
||||
)
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list, suite_name="My Suite")
|
||||
|
||||
result = suite_result.to_report_dict()
|
||||
|
||||
assert result["suite_passed"] is False
|
||||
assert result["items_passed"] == 0
|
||||
assert result["items_total"] == 1
|
||||
assert result["pass_rate"] == 0.0
|
||||
assert result["experiment_id"] == "exp-123"
|
||||
assert result["experiment_name"] == "my-experiment"
|
||||
assert result["experiment_url"] == "http://example.com/experiment/exp-123"
|
||||
assert result["suite_name"] == "My Suite"
|
||||
assert "generated_at" in result
|
||||
|
||||
assert len(result["items"]) == 1
|
||||
item = result["items"][0]
|
||||
assert item["dataset_item_id"] == "item-1"
|
||||
assert item["passed"] is False
|
||||
assert item["runs_passed"] == 0
|
||||
assert item["execution_policy"] == {"runs_per_item": 1, "pass_threshold": 1}
|
||||
|
||||
assert len(item["runs"]) == 1
|
||||
run = item["runs"][0]
|
||||
assert run["trial_id"] == 0
|
||||
assert run["passed"] is False
|
||||
assert run["input"] == "hi"
|
||||
assert run["output"] == "hello"
|
||||
assert run["trace_id"] == "trace-item-1-0"
|
||||
assert run["task_execution_time_seconds"] == 1.234
|
||||
assert run["scoring_time_seconds"] == 0.567
|
||||
|
||||
assert len(run["assertions"]) == 2
|
||||
assert run["assertions"][0]["name"] == "Is polite"
|
||||
assert run["assertions"][0]["passed"] is True
|
||||
assert run["assertions"][1]["name"] == "Is helpful"
|
||||
assert run["assertions"][1]["passed"] is False
|
||||
|
||||
def test_to_report_dict__all_items_pass__suite_passed_true(self):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
_make_test_result(
|
||||
dataset_item_id="item-2",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
|
||||
result = suite_result.to_report_dict()
|
||||
|
||||
assert result["suite_passed"] is True
|
||||
assert result["items_passed"] == 2
|
||||
assert result["pass_rate"] == 1.0
|
||||
|
||||
def test_to_report_dict__with_total_time__includes_rounded_value(self):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list, total_time=12.3456)
|
||||
|
||||
result = suite_result.to_report_dict()
|
||||
|
||||
assert result["total_time_seconds"] == 12.346
|
||||
|
||||
def test_to_report_dict__scoring_failed__marks_assertion_failed_with_reason(self):
|
||||
test_results_list = [
|
||||
test_result.TestResult(
|
||||
test_case=test_case.TestCase(
|
||||
trace_id="trace-1",
|
||||
dataset_item_id="item-1",
|
||||
task_output={"output": "test"},
|
||||
dataset_item_content={},
|
||||
dataset_item=dataset_item.DatasetItem(
|
||||
id="item-1",
|
||||
execution_policy=dataset_item.ExecutionPolicyItem(
|
||||
runs_per_item=1,
|
||||
pass_threshold=1,
|
||||
),
|
||||
),
|
||||
),
|
||||
score_results=[
|
||||
score_result.ScoreResult(
|
||||
name="A1",
|
||||
value=0,
|
||||
scoring_failed=True,
|
||||
reason="Model error",
|
||||
),
|
||||
],
|
||||
trial_id=0,
|
||||
)
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
|
||||
result = suite_result.to_report_dict()
|
||||
|
||||
assertion = result["items"][0]["runs"][0]["assertions"][0]
|
||||
assert assertion["passed"] is False
|
||||
assert assertion["scoring_failed"] is True
|
||||
assert assertion["reason"] == "Model error"
|
||||
|
||||
|
||||
class TestSaveReport:
|
||||
def test_save_report__valid_input__writes_json_file(self, tmp_path):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(
|
||||
test_results_list, suite_name="Test Suite", total_time=5.0
|
||||
)
|
||||
output_path = str(tmp_path / "report.json")
|
||||
|
||||
result_path = file_writer.save_report(suite_result, output_path)
|
||||
|
||||
assert result_path == output_path
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
with open(output_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert data["suite_name"] == "Test Suite"
|
||||
assert data["suite_passed"] is True
|
||||
assert len(data["items"]) == 1
|
||||
|
||||
def test_save_report__no_path__uses_experiment_name(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
|
||||
result_path = file_writer.save_report(suite_result)
|
||||
|
||||
assert "my-experiment" in os.path.basename(result_path)
|
||||
assert result_path.endswith(".json")
|
||||
assert os.path.exists(result_path)
|
||||
|
||||
def test_save_report__nested_path__creates_parent_directories(self, tmp_path):
|
||||
output_path = str(tmp_path / "nested" / "dir" / "report.json")
|
||||
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
|
||||
result_path = file_writer.save_report(suite_result, output_path)
|
||||
|
||||
assert os.path.exists(result_path)
|
||||
|
||||
|
||||
class TestBuildDefaultReportPath:
|
||||
def test_build_default_report_path__unsafe_characters__replaces_with_underscore(
|
||||
self,
|
||||
):
|
||||
path = file_writer.build_default_report_path("my/suite:name")
|
||||
assert os.path.basename(path) == "my_suite_name.json"
|
||||
|
||||
def test_build_default_report_path__safe_characters__keeps_unchanged(self):
|
||||
path = file_writer.build_default_report_path("my-suite_v1.0")
|
||||
assert os.path.basename(path) == "my-suite_v1.0.json"
|
||||
|
||||
def test_build_default_report_path__spaces__replaces_with_underscore(self):
|
||||
path = file_writer.build_default_report_path("my suite")
|
||||
assert os.path.basename(path) == "my_suite.json"
|
||||
|
||||
|
||||
class TestTestSuiteResultMethods:
|
||||
def test_to_dict__passing_suite__returns_dict_with_suite_passed_true(self):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
|
||||
result = suite_result.to_dict()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["suite_passed"] is True
|
||||
assert "items" in result
|
||||
|
||||
def test_to_report_dict__save_to_file__produces_valid_json(self, tmp_path):
|
||||
test_results_list = [
|
||||
_make_test_result(
|
||||
dataset_item_id="item-1",
|
||||
trial_id=0,
|
||||
scores=[("A1", True)],
|
||||
execution_policy={"runs_per_item": 1, "pass_threshold": 1},
|
||||
),
|
||||
]
|
||||
suite_result = _make_suite_result(test_results_list)
|
||||
output_path = str(tmp_path / "result.json")
|
||||
|
||||
path = file_writer.save_report(suite_result, output_path)
|
||||
|
||||
assert os.path.exists(path)
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
assert data["suite_passed"] is True
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user