Files
2026-07-13 13:35:10 +08:00

514 lines
18 KiB
Python

"""Comprehensive tests for LocalJSONLBackend to test serialization capabilities."""
import tempfile
import typing as t
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
import pytest
from pydantic import BaseModel
from ragas.backends.local_jsonl import LocalJSONLBackend
# Test BaseModel classes
class SimpleTestModel(BaseModel):
name: str
age: int
score: float
is_active: bool
class ComplexTestModel(BaseModel):
id: int
metadata: Dict[str, Any]
tags: List[str]
config: Optional[Dict[str, Any]] = None
created_at: datetime
class NestedTestModel(BaseModel):
user: SimpleTestModel
settings: Dict[str, Any]
history: List[Dict[str, Any]]
# Test fixtures
@pytest.fixture
def temp_dir():
"""Create a temporary directory for testing."""
with tempfile.TemporaryDirectory() as tmp_dir:
yield tmp_dir
@pytest.fixture(name="backend")
def jsonl_backend_fixture(temp_dir):
"""Create a LocalJSONLBackend instance with temp directory."""
return LocalJSONLBackend(temp_dir)
@pytest.fixture
def simple_data():
"""Simple test data with basic types."""
return [
{"name": "Alice", "age": 30, "score": 85.5, "is_active": True},
{"name": "Bob", "age": 25, "score": 92.0, "is_active": False},
{"name": "Charlie", "age": 35, "score": 78.5, "is_active": True},
]
@pytest.fixture
def complex_data():
"""Complex test data with nested structures."""
return [
{
"id": 1,
"metadata": {"score": 0.85, "tags": ["test", "important"]},
"tags": ["evaluation", "metrics"],
"config": {"model": "gpt-4", "temperature": 0.7},
"created_at": datetime(2024, 1, 15, 10, 30, 0),
},
{
"id": 2,
"metadata": {"score": 0.92, "tags": ["production"]},
"tags": ["benchmark", "validation"],
"config": {"model": "claude-3", "temperature": 0.5},
"created_at": datetime(2024, 1, 16, 14, 45, 0),
},
]
@pytest.fixture
def nested_data():
"""Deeply nested test data."""
return [
{
"user": {"name": "Alice", "age": 30, "score": 85.5, "is_active": True},
"settings": {
"theme": "dark",
"notifications": {"email": True, "push": False},
"features": ["advanced", "beta"],
},
"history": [
{"action": "login", "timestamp": "2024-01-15T10:30:00"},
{"action": "query", "timestamp": "2024-01-15T10:35:00"},
],
}
]
# 1. Basic Functionality Tests
class TestBasicFunctionality:
"""Test basic LocalJSONLBackend functionality."""
def test_initialization(self, temp_dir):
"""Test backend initialization."""
backend = LocalJSONLBackend(temp_dir)
assert backend.root_dir == Path(temp_dir)
def test_get_data_dir(self, backend):
"""Test data directory path generation."""
datasets_dir = backend._get_data_dir("datasets")
experiments_dir = backend._get_data_dir("experiments")
assert datasets_dir.name == "datasets"
assert experiments_dir.name == "experiments"
def test_get_file_path(self, backend):
"""Test file path generation."""
dataset_path = backend._get_file_path("datasets", "test_dataset")
experiment_path = backend._get_file_path("experiments", "test_experiment")
assert dataset_path.name == "test_dataset.jsonl"
assert experiment_path.name == "test_experiment.jsonl"
def test_save_and_load_simple_data(self, backend, simple_data):
"""Test basic save and load cycle with simple data."""
# Save dataset
backend.save_dataset("test_simple", simple_data)
# Load dataset
loaded_data = backend.load_dataset("test_simple")
# Verify data structure - JSONL should preserve types
assert len(loaded_data) == len(simple_data)
assert loaded_data[0]["name"] == "Alice"
assert loaded_data[0]["age"] == 30 # Should be int, not string
assert loaded_data[0]["score"] == 85.5 # Should be float, not string
assert loaded_data[0]["is_active"] is True # Should be bool, not string
def test_directory_creation(self, backend, simple_data):
"""Test automatic directory creation."""
# Directories shouldn't exist initially
datasets_dir = backend._get_data_dir("datasets")
experiments_dir = backend._get_data_dir("experiments")
assert not datasets_dir.exists()
assert not experiments_dir.exists()
# Save data should create directories
backend.save_dataset("test", simple_data)
backend.save_experiment("test", simple_data)
# Directories should now exist
assert datasets_dir.exists()
assert experiments_dir.exists()
def test_list_datasets_and_experiments(self, backend, simple_data):
"""Test listing datasets and experiments."""
# Initially empty
assert backend.list_datasets() == []
assert backend.list_experiments() == []
# Save some data
backend.save_dataset("dataset1", simple_data)
backend.save_dataset("dataset2", simple_data)
backend.save_experiment("experiment1", simple_data)
# Check listings
datasets = backend.list_datasets()
experiments = backend.list_experiments()
assert sorted(datasets) == ["dataset1", "dataset2"]
assert experiments == ["experiment1"]
def test_save_empty_data(self, backend):
"""Test saving empty datasets."""
backend.save_dataset("empty_dataset", [])
# Should create empty file
file_path = backend._get_file_path("datasets", "empty_dataset")
assert file_path.exists()
# Loading should return empty list
loaded_data = backend.load_dataset("empty_dataset")
assert loaded_data == []
# 2. Data Type Edge Cases (The Real Challenge)
class TestDataTypeEdgeCases:
"""Test complex data types that JSONL should handle properly."""
def test_nested_dictionaries(self, backend):
"""Test nested dictionary serialization - JSONL should handle this."""
data = [
{
"id": 1,
"metadata": {"score": 0.85, "tags": ["test", "important"]},
"config": {"model": "gpt-4", "settings": {"temperature": 0.7}},
}
]
backend.save_dataset("nested_test", data)
loaded_data = backend.load_dataset("nested_test")
# JSONL should preserve nested dictionaries exactly
assert loaded_data[0]["metadata"] == {
"score": 0.85,
"tags": ["test", "important"],
}
assert loaded_data[0]["config"]["settings"]["temperature"] == 0.7
def test_lists_of_objects(self, backend):
"""Test lists of objects serialization - JSONL should handle this."""
data = [
{
"id": 1,
"results": [
{"metric": "accuracy", "value": 0.9},
{"metric": "precision", "value": 0.8},
],
}
]
backend.save_dataset("list_test", data)
loaded_data = backend.load_dataset("list_test")
# JSONL should preserve lists of objects
assert loaded_data[0]["results"][0]["metric"] == "accuracy"
assert loaded_data[0]["results"][0]["value"] == 0.9
assert loaded_data[0]["results"][1]["metric"] == "precision"
assert loaded_data[0]["results"][1]["value"] == 0.8
def test_mixed_types(self, backend):
"""Test mixed data types - JSONL should preserve all types."""
data = [
{
"str_field": "text",
"int_field": 42,
"float_field": 3.14,
"bool_field": True,
"null_field": None,
}
]
backend.save_dataset("mixed_test", data)
loaded_data = backend.load_dataset("mixed_test")
# JSONL should preserve all data types
assert loaded_data[0]["str_field"] == "text"
assert loaded_data[0]["int_field"] == 42 # Should be int
assert loaded_data[0]["float_field"] == 3.14 # Should be float
assert loaded_data[0]["bool_field"] is True # Should be bool
assert loaded_data[0]["null_field"] is None # Should be None
def test_datetime_objects(self, backend):
"""Test datetime serialization - JSONL should handle this with ISO format."""
data = [
{
"id": 1,
"created_at": datetime(2024, 1, 15, 10, 30, 0),
"updated_date": date(2024, 1, 16),
}
]
backend.save_dataset("datetime_test", data)
loaded_data = backend.load_dataset("datetime_test")
# JSONL should either preserve datetime objects or convert to ISO strings
# For now, let's expect ISO strings that can be parsed back
original_dt = data[0]["created_at"]
loaded_dt = loaded_data[0]["created_at"]
# Should be either datetime object or ISO string
assert isinstance(original_dt, datetime)
if isinstance(loaded_dt, str):
# If string, should be valid ISO format
parsed_dt = datetime.fromisoformat(loaded_dt.replace("Z", "+00:00"))
assert parsed_dt.year == 2024
assert parsed_dt.month == 1
assert parsed_dt.day == 15
else:
# If datetime object, should be exact match
assert loaded_dt == original_dt
def test_complex_nested_structure(self, backend):
"""Test deeply nested structures - JSONL should handle this perfectly."""
data = [
{
"config": {
"database": {
"host": "localhost",
"ports": [5432, 5433],
"credentials": {"user": "admin", "encrypted": True},
},
"features": ["auth", "logging"],
}
}
]
backend.save_dataset("complex_test", data)
loaded_data = backend.load_dataset("complex_test")
# JSONL should preserve complex nested structures exactly
assert loaded_data[0]["config"]["database"]["host"] == "localhost"
assert loaded_data[0]["config"]["database"]["ports"] == [5432, 5433]
assert loaded_data[0]["config"]["database"]["credentials"]["user"] == "admin"
assert loaded_data[0]["config"]["database"]["credentials"]["encrypted"] is True
assert loaded_data[0]["config"]["features"] == ["auth", "logging"]
# 3. BaseModel Integration Tests
class TestBaseModelIntegration:
"""Test BaseModel validation and conversion."""
def test_simple_basemodel_save_load(self, backend, simple_data):
"""Test BaseModel with simple data types."""
# Save raw data
backend.save_dataset("simple_model_test", simple_data, SimpleTestModel)
# Load and validate with BaseModel
loaded_data = backend.load_dataset("simple_model_test")
# JSONL should enable perfect BaseModel roundtrip
models = [SimpleTestModel(**item) for item in loaded_data]
assert len(models) == 3
assert models[0].name == "Alice"
assert models[0].age == 30
assert models[0].score == 85.5
assert models[0].is_active is True
def test_complex_basemodel_roundtrip(self, backend, complex_data):
"""Test BaseModel with complex data - JSONL should handle this."""
# Save raw data
backend.save_dataset("complex_model_test", complex_data, ComplexTestModel)
# Load and try to validate
loaded_data = backend.load_dataset("complex_model_test")
# JSONL should enable perfect BaseModel validation
models = [ComplexTestModel(**item) for item in loaded_data]
assert len(models) == 2
assert models[0].id == 1
assert models[0].metadata["score"] == 0.85
assert models[0].tags == ["evaluation", "metrics"]
assert models[0].config is not None and models[0].config["model"] == "gpt-4"
def test_basemodel_type_coercion(self, backend):
"""Test BaseModel's ability to coerce string types."""
# Data that should be coercible from strings
data = [{"name": "Alice", "age": "30", "score": "85.5", "is_active": "true"}]
backend.save_dataset("coercion_test", data)
loaded_data = backend.load_dataset("coercion_test")
# JSONL + Pydantic should handle type coercion perfectly
model = SimpleTestModel(**loaded_data[0])
assert model.name == "Alice"
assert model.age == 30 # String "30" -> int 30
assert model.score == 85.5 # String "85.5" -> float 85.5
# Note: "true" -> bool True coercion depends on implementation
# 4. Error Handling & Edge Cases
class TestErrorHandling:
"""Test error scenarios and edge cases."""
def test_load_nonexistent_file(self, backend):
"""Test loading non-existent files."""
with pytest.raises(FileNotFoundError):
backend.load_dataset("nonexistent")
with pytest.raises(FileNotFoundError):
backend.load_experiment("nonexistent")
def test_unicode_and_special_characters(self, backend):
"""Test handling of unicode and special characters."""
data = [
{
"name": "José María",
"description": "Testing émojis 🚀 and spëcial chars",
"chinese": "你好世界",
"symbols": "!@#$%^&*()_+{}[]|;:,.<>?",
}
]
backend.save_dataset("unicode_test", data)
loaded_data = backend.load_dataset("unicode_test")
# Unicode should be preserved perfectly in JSONL
assert loaded_data[0]["name"] == "José María"
assert loaded_data[0]["chinese"] == "你好世界"
assert "🚀" in loaded_data[0]["description"]
def test_json_special_characters(self, backend):
"""Test handling of JSON special characters."""
data = [
{
"quotes": 'He said "Hello World"',
"backslashes": "C:\\Users\\test\\file.txt",
"newlines": "Line 1\nLine 2\nLine 3",
"tabs": "Column1\tColumn2\tColumn3",
}
]
backend.save_dataset("special_chars_test", data)
loaded_data = backend.load_dataset("special_chars_test")
# JSONL should handle JSON special characters properly
assert loaded_data[0]["quotes"] == 'He said "Hello World"'
assert loaded_data[0]["backslashes"] == "C:\\Users\\test\\file.txt"
assert loaded_data[0]["newlines"] == "Line 1\nLine 2\nLine 3"
assert loaded_data[0]["tabs"] == "Column1\tColumn2\tColumn3"
def test_empty_and_null_values(self, backend):
"""Test handling of empty and null values."""
data = [
{
"empty_string": "",
"null_value": None,
"whitespace": " ",
"zero": 0,
"false": False,
}
]
backend.save_dataset("empty_test", data)
loaded_data = backend.load_dataset("empty_test")
# JSONL should handle null values properly
assert loaded_data[0]["empty_string"] == ""
assert loaded_data[0]["null_value"] is None
assert loaded_data[0]["whitespace"] == " "
assert loaded_data[0]["zero"] == 0
assert loaded_data[0]["false"] is False
def test_large_text_fields(self, backend):
"""Test handling of large text fields."""
large_text = "A" * 10000 # 10KB of text
data = [
{
"id": 1,
"large_field": large_text,
"normal_field": "small",
}
]
backend.save_dataset("large_text_test", data)
loaded_data = backend.load_dataset("large_text_test")
# Large text should be preserved perfectly
assert len(loaded_data[0]["large_field"]) == 10000
assert loaded_data[0]["large_field"] == large_text
def test_malformed_jsonl_handling(self, backend, temp_dir):
"""Test behavior with malformed JSONL files."""
# Create a malformed JSONL file manually
malformed_jsonl = Path(temp_dir) / "datasets" / "malformed.jsonl"
malformed_jsonl.parent.mkdir(parents=True, exist_ok=True)
with open(malformed_jsonl, "w") as f:
f.write('{"valid": "json"}\n')
f.write('{"invalid": json}\n') # Invalid JSON
f.write('{"another": "valid"}\n')
# Try to load malformed JSONL
try:
loaded_data = backend.load_dataset("malformed")
# Should either handle gracefully or raise appropriate error
print(f"Malformed JSONL loaded: {loaded_data}")
except Exception as e:
print(f"Malformed JSONL failed to load: {e}")
# This is acceptable behavior
# Helper functions for debugging
def print_jsonl_content(jsonl_backend, data_type, name):
"""Helper to print raw JSONL content for debugging."""
file_path = backend._get_file_path(data_type, name)
if file_path.exists():
print(f"\n=== JSONL Content for {name} ===")
with open(file_path, "r") as f:
print(f.read())
print("=== End JSONL Content ===\n")
if __name__ == "__main__":
# Run some quick tests to see JSONL capabilities
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
try:
backend: LocalJSONLBackend = LocalJSONLBackend(tmp_dir)
# Test nested data
test_nested_data: list[dict[str, t.Any]] = [
{"id": 1, "metadata": {"score": 0.85, "tags": ["test"]}}
]
backend.save_dataset("debug_nested", test_nested_data)
loaded = backend.load_dataset("debug_nested")
print("=== Nested Data Test ===")
print(f"Original: {test_nested_data[0]['metadata']}")
print(f"Loaded: {loaded[0]['metadata']}")
print(
f"Types: {type(test_nested_data[0]['metadata'])} -> {type(loaded[0]['metadata'])}"
)
print_jsonl_content(backend, "datasets", "debug_nested")
except ImportError as e:
print(f"Expected ImportError: {e}")
except Exception as e:
print(f"Unexpected error: {e}")