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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:44 +08:00
commit 5a558eb09e
11579 changed files with 1795921 additions and 0 deletions
@@ -0,0 +1,432 @@
import pandas as pd
import pandas.testing
import json
import tempfile
import os
from opik.api_objects.dataset import converters
from opik.api_objects.dataset.dataset_item import DatasetItem
from ....testlib import ANY_BUT_NONE
def test_from_pandas__all_columns_from_dataframe_represent_all_dataset_item_fields():
data_for_dataframe = {
"id": ["id-1", "id-2"],
"input": [{"input-key-1": "input-1"}, {"input-key-2": "input-2"}],
"expected_output": [
{"expected-output-key-1": "expected-output-1"},
{"expected-output-key-2": "expected-output-2"},
],
"metadata": [{"metadata-key-1": "v1"}, {"metadata-key-2": "v2"}],
"span_id": ["span-id-1", "span-id-2"],
"trace_id": ["trace-id-1", "trace-id-2"],
"source": ["some-source-1", "some-source-2"],
}
EXPECTED_ITEMS = [
DatasetItem(
id="id-1",
input={"input-key-1": "input-1"},
expected_output={"expected-output-key-1": "expected-output-1"},
metadata={"metadata-key-1": "v1"},
span_id="span-id-1",
trace_id="trace-id-1",
source="some-source-1",
),
DatasetItem(
id="id-2",
input={"input-key-2": "input-2"},
expected_output={"expected-output-key-2": "expected-output-2"},
metadata={"metadata-key-2": "v2"},
span_id="span-id-2",
trace_id="trace-id-2",
source="some-source-2",
),
]
dataframe = pd.DataFrame(data_for_dataframe)
actual_items = converters.from_pandas(
dataframe=dataframe, keys_mapping={}, ignore_keys=[]
)
assert actual_items == EXPECTED_ITEMS
def test_from_pandas__only_input_presented_in_dataframe__items_are_constructed_with_default_values_for_missing_fields():
data_for_dataframe = {
"input": [{"input-key-1": "input-1"}, {"input-key-2": "input-2"}],
}
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
dataframe = pd.DataFrame(data_for_dataframe)
actual_items = converters.from_pandas(
dataframe=dataframe, keys_mapping={}, ignore_keys=[]
)
assert actual_items == EXPECTED_ITEMS
def test_from_pandas__dataframe_column_does_not_have_the_same_name_as_dataset_item_field__keys_mapping_is_used():
data_for_dataframe = {
"Input column name": [{"input-key-1": "input-1"}, {"input-key-2": "input-2"}],
}
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
dataframe = pd.DataFrame(data_for_dataframe)
actual_items = converters.from_pandas(
dataframe=dataframe, keys_mapping={"Input column name": "input"}, ignore_keys=[]
)
assert actual_items == EXPECTED_ITEMS
def test_from_pandas__dataframe_contains_extra_column_not_needed_for_dataset_item__ignore_keys_is_used():
data_for_dataframe = {
"input": [{"input-key-1": "input-1"}, {"input-key-2": "input-2"}],
"some-extra-column": [1, 2],
}
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
dataframe = pd.DataFrame(data_for_dataframe)
actual_items = converters.from_pandas(
dataframe=dataframe, keys_mapping={}, ignore_keys=["some-extra-column"]
)
assert actual_items == EXPECTED_ITEMS
def test_to_pandas__with_keys_mapping__span_id_trace_id_and_source_ignored__happyflow():
EXPECTED_DATAFRAME = pd.DataFrame(
{
"id": ["id-1", "id-2"],
"input": [{"input-key-1": "input-1"}, {"input-key-2": "input-2"}],
"Customized expected output": [
{"expected-output-key-1": "expected-output-1"},
{"expected-output-key-2": "expected-output-2"},
],
"metadata": [{"metadata-key-1": "v1"}, {"metadata-key-2": "v2"}],
}
)
input_items = [
DatasetItem(
id="id-1",
input={"input-key-1": "input-1"},
expected_output={"expected-output-key-1": "expected-output-1"},
metadata={"metadata-key-1": "v1"},
span_id="span-id-1",
trace_id="trace-id-1",
source="some-source-1",
),
DatasetItem(
id="id-2",
input={"input-key-2": "input-2"},
expected_output={"expected-output-key-2": "expected-output-2"},
metadata={"metadata-key-2": "v2"},
span_id="span-id-2",
trace_id="trace-id-2",
source="some-source-2",
),
]
actual_dataframe = converters.to_pandas(
input_items, keys_mapping={"expected_output": "Customized expected output"}
)
# check_like ignores columns and rows order
pandas.testing.assert_frame_equal(
actual_dataframe, EXPECTED_DATAFRAME, check_like=True
)
def test_from_json__all_columns_from_dataframe_represent_all_dataset_item_fields():
input_json = """
[
{
"id": "id-1",
"input": {"input-key-1": "input-1"},
"expected_output": {"expected-output-key-1": "expected-output-1"},
"metadata": {"metadata-key-1": "v1"}
},
{
"id": "id-2",
"input": {"input-key-2": "input-2"},
"expected_output": {"expected-output-key-2": "expected-output-2"},
"metadata": {"metadata-key-2": "v2"}
}
]
"""
EXPECTED_ITEMS = [
DatasetItem(
id="id-1",
input={"input-key-1": "input-1"},
expected_output={"expected-output-key-1": "expected-output-1"},
metadata={"metadata-key-1": "v1"},
),
DatasetItem(
id="id-2",
input={"input-key-2": "input-2"},
expected_output={"expected-output-key-2": "expected-output-2"},
metadata={"metadata-key-2": "v2"},
),
]
actual_items = converters.from_json(input_json, keys_mapping={}, ignore_keys=[])
assert actual_items == EXPECTED_ITEMS
def test_from_json__only_input_presented_in_json__items_are_constructed_with_default_values_for_missing_fields():
input_json = """
[
{
"input": {"input-key-1": "input-1"}
},
{
"input": {"input-key-2": "input-2"}
}
]
"""
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
actual_items = converters.from_json(input_json, keys_mapping={}, ignore_keys=[])
assert actual_items == EXPECTED_ITEMS
def test_from_json__json_objects_contain_extra_key_not_needed_for_dataset_item__ignore_keys_is_used():
input_json = """
[
{
"input": {"input-key-1": "input-1"},
"extra_key": 42
},
{
"input": {"input-key-2": "input-2"},
"extra_key": 4242
}
]
"""
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
actual_items = converters.from_json(
input_json, keys_mapping={}, ignore_keys=["extra_key"]
)
assert actual_items == EXPECTED_ITEMS
def test_from_json__json_objects_dont_have_the_same_name_as_dataset_item_field__keys_mapping_is_used():
input_json = """
[
{
"JSON input key": {"input-key-1": "input-1"}
},
{
"JSON input key": {"input-key-2": "input-2"}
}
]
"""
EXPECTED_ITEMS = [
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-1": "input-1"},
source="sdk",
),
DatasetItem(
id=ANY_BUT_NONE,
input={"input-key-2": "input-2"},
source="sdk",
),
]
actual_items = converters.from_json(
input_json, keys_mapping={"JSON input key": "input"}, ignore_keys=[]
)
assert actual_items == EXPECTED_ITEMS
def test_to_json__with_keys_mapping__span_id_trace_id_and_source_ignored__happyflow():
EXPECTED_JSON = """
[
{
"id": "id-1",
"input": {"input-key-1": "input-1"},
"Customized expected output": {"expected-output-key-1": "expected-output-1"},
"metadata": {"metadata-key-1": "v1"}
},
{
"id": "id-2",
"input": {"input-key-2": "input-2"},
"Customized expected output": {"expected-output-key-2": "expected-output-2"},
"metadata": {"metadata-key-2": "v2"}
}
]
"""
input_items = [
DatasetItem(
id="id-1",
input={"input-key-1": "input-1"},
expected_output={"expected-output-key-1": "expected-output-1"},
metadata={"metadata-key-1": "v1"},
span_id="span-id-1",
trace_id="trace-id-1",
source="some-source-1",
),
DatasetItem(
id="id-2",
input={"input-key-2": "input-2"},
expected_output={"expected-output-key-2": "expected-output-2"},
metadata={"metadata-key-2": "v2"},
span_id="span-id-2",
trace_id="trace-id-2",
source="some-source-2",
),
]
actual_json = converters.to_json(
input_items, keys_mapping={"expected_output": "Customized expected output"}
)
assert json.loads(actual_json) == json.loads(EXPECTED_JSON)
def test_from_jsonl_file__happyflow():
jsonl_content = """
{"input": {"user_question": "What is the capital of France?"}, "expected_output": {"assistant_answer": "The capital of France is Paris."}}
{"input": {"user_question": "How many planets are in our solar system?"}, "expected_output": {"assistant_answer": "There are 8 planets in our solar system: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune."}}
"""
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
temp_file.write(jsonl_content)
temp_file_path = temp_file.name
try:
result = converters.from_jsonl_file(
temp_file_path, keys_mapping={}, ignore_keys=[]
)
assert result[0].input == {"user_question": "What is the capital of France?"}
assert result[0].expected_output == {
"assistant_answer": "The capital of France is Paris."
}
assert result[1].input == {
"user_question": "How many planets are in our solar system?"
}
assert result[1].expected_output == {
"assistant_answer": "There are 8 planets in our solar system: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune."
}
finally:
os.unlink(temp_file_path)
def test_from_jsonl_file__empty_file():
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
temp_file_path = temp_file.name
try:
result = converters.from_jsonl_file(
temp_file_path, keys_mapping={}, ignore_keys=[]
)
assert isinstance(result, list)
assert len(result) == 0
finally:
os.unlink(temp_file_path)
def test_from_jsonl_file__file_with_empty_lines():
jsonl_content = """
{"input": {"user_question": "What is the capital of France?"}, "expected_output": {"assistant_answer": "The capital of France is Paris."}}
{"input": {"user_question": "How many planets are in our solar system?"}, "expected_output": {"assistant_answer": "There are 8 planets in our solar system: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune."}}
"""
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
temp_file.write(jsonl_content)
temp_file_path = temp_file.name
try:
result = converters.from_jsonl_file(
temp_file_path, keys_mapping={}, ignore_keys=[]
)
assert len(result) == 2
assert result[0].input == {"user_question": "What is the capital of France?"}
assert result[0].expected_output == {
"assistant_answer": "The capital of France is Paris."
}
assert result[1].input == {
"user_question": "How many planets are in our solar system?"
}
assert result[1].expected_output == {
"assistant_answer": "There are 8 planets in our solar system: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune."
}
finally:
os.unlink(temp_file_path)
@@ -0,0 +1,165 @@
from unittest.mock import Mock
from opik.api_objects.dataset.dataset import Dataset
def test_insert_deduplication__two_dicts_passed_with_the_same_content__only_one_is_inserted():
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
)
item_dict = {
"input": {"key": "value", "key2": "value2"},
"expected_output": {"key": "value", "key2": "value2"},
"metadata": {"key": "value", "key2": "value2"},
}
# Insert the identical items
dataset.insert([item_dict, item_dict])
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 1, (
"create_or_update_dataset_items should be called only once"
)
call_args = mock_rest_client.datasets.create_or_update_dataset_items.call_args
inserted_items = call_args[1]["items"]
assert len(inserted_items) == 1, "Only one item should be inserted"
def test_insert_deduplication__two_dicts_passed_with_the_different_content__both_are_inserted():
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
)
item_dict1 = {
"input": {"key": "value1"},
"expected_output": {"key": "output1"},
"metadata": {"key": "meta1"},
}
item_dict2 = {
"input": {"key": "value2"},
"expected_output": {"key": "output2"},
"metadata": {"key": "meta2"},
}
# Insert the different items
dataset.insert([item_dict1, item_dict2])
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 1, (
"create_or_update_dataset_items should be called only once"
)
call_args = mock_rest_client.datasets.create_or_update_dataset_items.call_args
inserted_items = call_args[1]["items"]
assert len(inserted_items) == 2, "Two items should be inserted"
def test_insert_deduplication__three_dicts_passed__one_unique__two_duplicates__two_different_items_are_inserted():
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
)
item_dict1 = {
"input": {"key": "value1"},
"expected_output": {"key": "output1"},
"metadata": {"key": "meta1"},
}
item_dict2 = {
"input": {"key": "value2"},
"expected_output": {"key": "output2"},
"metadata": {"key": "meta2"},
}
# Insert 3 items: one unique and two duplicates
dataset.insert([item_dict1, item_dict2, item_dict1])
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 1, (
"create_or_update_dataset_items should be called only once"
)
call_args = mock_rest_client.datasets.create_or_update_dataset_items.call_args
inserted_rest_items = call_args[1]["items"]
assert len(inserted_rest_items) == 2, "Two items should be inserted"
def test_update__happyflow():
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
)
initial_item = {
"input": {"key": "initial_value"},
"expected_output": {"key": "initial_output"},
"metadata": {"key": "initial_metadata"},
}
dataset.insert([initial_item])
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 1, (
"create_or_update_dataset_items should be called once for insertion"
)
insert_call_args = (
mock_rest_client.datasets.create_or_update_dataset_items.call_args
)
inserted_items = insert_call_args[1]["items"]
assert len(inserted_items) == 1, "One item should be inserted"
# Create an updated version of the item
updated_item = {
"id": inserted_items[0].id,
"input": {"key": "updated_value"},
"expected_output": {"key": "updated_output"},
"metadata": {"key": "updated_metadata"},
}
# Update the item
dataset.update([updated_item])
# Check that create_or_update_dataset_items was called twice in total (once for insertion, once for update)
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 2, (
"create_or_update_dataset_items should be called twice in total"
)
# Get the arguments passed to create_or_update_dataset_items for update
update_call_args = (
mock_rest_client.datasets.create_or_update_dataset_items.call_args
)
updated_rest_items = update_call_args[1]["items"]
# Check that one item was updated
assert len(updated_rest_items) == 1, "One item should be updated"
# Verify the content of the updated item
assert updated_rest_items[0].data["input"] == {"key": "updated_value"}, (
"Input should be updated"
)
assert updated_rest_items[0].data["expected_output"] == {"key": "updated_output"}, (
"Expected output should be updated"
)
assert updated_rest_items[0].data["metadata"] == {"key": "updated_metadata"}, (
"Metadata should be updated"
)
@@ -0,0 +1,178 @@
from unittest.mock import Mock
from opik.api_objects.dataset.dataset import Dataset
from opik.rest_api.types.dataset_public import DatasetPublic
def test_dataset_items_count__cached_value__returns_cached_count():
"""Test that dataset_items_count returns cached value when available."""
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=5,
)
count = dataset.dataset_items_count
assert count == 5
mock_rest_client.datasets.get_dataset_by_id.assert_not_called()
def test_dataset_items_count__no_cached_value__fetches_from_backend():
"""Test that dataset_items_count fetches from backend when cache is None."""
mock_rest_client = Mock()
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=10)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=None,
)
count = dataset.dataset_items_count
assert count == 10
mock_rest_client.datasets.get_dataset_by_id.assert_called_once_with(id=dataset.id)
def test_dataset_items_count__fetched_once__cached_for_subsequent_calls():
"""Test that dataset_items_count is fetched once and then cached."""
mock_rest_client = Mock()
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=10)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=None,
)
count1 = dataset.dataset_items_count
count2 = dataset.dataset_items_count
count3 = dataset.dataset_items_count
assert count1 == 10
assert count2 == 10
assert count3 == 10
mock_rest_client.datasets.get_dataset_by_id.assert_called_once()
def test_delete__invalidates_cached_count():
"""Test that delete() invalidates the cached count."""
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=5,
)
assert dataset.dataset_items_count == 5
dataset.delete(["item1", "item2"])
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=3)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
count = dataset.dataset_items_count
assert count == 3
mock_rest_client.datasets.get_dataset_by_id.assert_called_once()
def test_update__invalidates_cached_count():
"""Test that update() invalidates the cached count."""
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=5,
)
assert dataset.dataset_items_count == 5
updated_item = {
"id": "item1",
"input": {"key": "updated_value"},
"expected_output": {"key": "updated_output"},
}
dataset.update([updated_item])
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=5)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
count = dataset.dataset_items_count
assert count == 5
mock_rest_client.datasets.get_dataset_by_id.assert_called_once()
def test_insert__invalidates_cached_count():
"""Test that insert() invalidates the cached count."""
mock_rest_client = Mock()
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=5,
)
assert dataset.dataset_items_count == 5
new_items = [
{
"input": {"key": "value1"},
"expected_output": {"key": "output1"},
},
{
"input": {"key": "value2"},
"expected_output": {"key": "output2"},
},
]
dataset.insert(new_items)
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=7)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
count = dataset.dataset_items_count
assert count == 7
mock_rest_client.datasets.get_dataset_by_id.assert_called_once()
def test_backend_returns_none_count__property_returns_none():
"""Test that if backend returns None for count, property returns None."""
mock_rest_client = Mock()
mock_dataset_public = DatasetPublic(name="test_dataset", dataset_items_count=None)
mock_rest_client.datasets.get_dataset_by_id.return_value = mock_dataset_public
dataset = Dataset(
name="test_dataset",
description="Test description",
project_name="Test project",
rest_client=mock_rest_client,
dataset_items_count=None,
)
count = dataset.dataset_items_count
assert count is None
mock_rest_client.datasets.get_dataset_by_id.assert_called_once()
@@ -0,0 +1,122 @@
import unittest
from unittest.mock import Mock, patch
from opik.api_objects.dataset import dataset
from opik.rest_api.core.api_error import ApiError
class TestDatasetRateLimitRetry(unittest.TestCase):
"""Test rate limit retry behavior for dataset operations using public API."""
def _create_dataset_with_mock_client(self) -> tuple[dataset.Dataset, Mock]:
"""Create a Dataset instance with a mocked REST client."""
mock_rest_client = Mock()
dataset_obj = dataset.Dataset(
name="test_dataset",
description="test",
project_name="Test project",
rest_client=mock_rest_client,
)
return dataset_obj, mock_rest_client
@patch("opik.api_objects.rest_helpers._sleep")
def test_insert__429_with_retry_after_header__retries_with_correct_delay(
self, mock_sleep: Mock
) -> None:
"""Test that 429 errors with RateLimit-Reset header are retried with correct delay."""
dataset_obj, mock_rest_client = self._create_dataset_with_mock_client()
# First call raises 429 with rate limit headers, second call succeeds
rate_limit_error = ApiError(
status_code=429,
headers={
"RateLimit-Reset": "5", # 5 seconds retry after
},
body="Rate limit exceeded",
)
mock_rest_client.datasets.create_or_update_dataset_items.side_effect = [
rate_limit_error,
None, # Success on second attempt
]
# Execute using public API
dataset_obj.insert([{"input": "test"}])
# Verify retry behavior
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 2
mock_sleep.assert_called_once_with(5.0)
@patch("opik.api_objects.rest_helpers._sleep")
def test_insert__429_without_header__uses_fallback_delay(
self, mock_sleep: Mock
) -> None:
"""Test that 429 errors without headers use fallback 1 second delay."""
dataset_obj, mock_rest_client = self._create_dataset_with_mock_client()
# First two calls raise 429 without headers, third succeeds
rate_limit_error = ApiError(
status_code=429,
headers={},
body="Rate limit exceeded",
)
mock_rest_client.datasets.create_or_update_dataset_items.side_effect = [
rate_limit_error,
rate_limit_error,
None, # Success on third attempt
]
# Execute using public API
dataset_obj.insert([{"input": "test"}])
# Verify fallback delay: always 1 second when no header
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 3
assert mock_sleep.call_count == 2
# Both retries should use 1 second delay
assert all(call[0][0] == 1 for call in mock_sleep.call_args_list)
def test_insert__non_429_error__raises_immediately(self) -> None:
"""Test that non-429 errors are raised immediately without retry."""
dataset_obj, mock_rest_client = self._create_dataset_with_mock_client()
# Simulate a 500 error
error = ApiError(
status_code=500,
headers={},
body="Internal server error",
)
mock_rest_client.datasets.create_or_update_dataset_items.side_effect = error
# Execute & Verify
with self.assertRaises(ApiError) as context:
dataset_obj.insert([{"input": "test"}])
assert context.exception.status_code == 500
# Should only try once for non-429 errors
assert mock_rest_client.datasets.create_or_update_dataset_items.call_count == 1
@patch("opik.api_objects.rest_helpers._sleep")
def test_delete__429_with_retry_after_header__retries_with_correct_delay(
self, mock_sleep: Mock
) -> None:
"""Test that delete operation also handles 429 errors correctly."""
dataset_obj, mock_rest_client = self._create_dataset_with_mock_client()
# First call raises 429 with rate limit headers, second call succeeds
rate_limit_error = ApiError(
status_code=429,
headers={
"RateLimit-Reset": "3",
},
body="Rate limit exceeded",
)
mock_rest_client.datasets.delete_dataset_items.side_effect = [
rate_limit_error,
None, # Success on second attempt
]
# Execute using public API
dataset_obj.delete(["item-id-1"])
# Verify retry behavior
assert mock_rest_client.datasets.delete_dataset_items.call_count == 2
mock_sleep.assert_called_once_with(3.0)
@@ -0,0 +1,74 @@
"""Unit tests for stream_dataset_items() in rest_operations."""
from unittest.mock import Mock, patch
from opik.api_objects.dataset import rest_operations
from opik.rest_api.types import dataset_item as rest_dataset_item
_SHADOW_WARNING = (
"Dataset item data contains keys that shadow DatasetItem fields and will be ignored: %s. "
"Rename these keys in your dataset to preserve them."
)
def _make_rest_item(item_id: str, data: dict) -> rest_dataset_item.DatasetItem:
return rest_dataset_item.DatasetItem(
id=item_id,
source="sdk",
data=data,
)
def test_stream_dataset_items__colliding_id_key__uses_real_id_and_warns():
real_id = "real-uuid-1234"
rest_item = _make_rest_item(real_id, {"id": "COLLISION", "question": "What?"})
mock_rest_client = Mock()
with (
patch(
"opik.api_objects.dataset.rest_operations.rest_stream_parser.read_and_parse_stream",
side_effect=[[rest_item], []],
),
patch.object(rest_operations.LOGGER, "warning") as mock_warn,
):
items = list(
rest_operations.stream_dataset_items(
rest_client=mock_rest_client,
dataset_name="test-dataset",
project_name=None,
)
)
assert len(items) == 1
assert items[0].id == real_id
mock_warn.assert_called_once_with(_SHADOW_WARNING, ["id"])
def test_stream_dataset_items__colliding_id_key__warning_emitted_only_once():
"""Warning is logged once per stream even when multiple items have the collision."""
items_data = [
_make_rest_item(f"uuid-{i}", {"id": f"hotpot-{i}", "question": "Q?"})
for i in range(3)
]
mock_rest_client = Mock()
with (
patch(
"opik.api_objects.dataset.rest_operations.rest_stream_parser.read_and_parse_stream",
side_effect=[items_data, []],
),
patch.object(rest_operations.LOGGER, "warning") as mock_warn,
):
result = list(
rest_operations.stream_dataset_items(
rest_client=mock_rest_client,
dataset_name="test-dataset",
project_name=None,
)
)
assert len(result) == 3
mock_warn.assert_called_once_with(_SHADOW_WARNING, ["id"])
@@ -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