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

469 lines
16 KiB
Python

"""Comprehensive tests for LocalCSVBackend to test serialization edge cases."""
import tempfile
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
import pytest
from pydantic import BaseModel, ValidationError
from ragas.backends.local_csv import LocalCSVBackend
# 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
def backend(temp_dir):
"""Create a LocalCSVBackend instance with temp directory."""
return LocalCSVBackend(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 LocalCSVBackend functionality."""
def test_initialization(self, temp_dir):
"""Test backend initialization."""
backend = LocalCSVBackend(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.csv"
assert experiment_path.name == "test_experiment.csv"
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 (note: all values become strings in CSV)
assert len(loaded_data) == len(simple_data)
assert loaded_data[0]["name"] == "Alice"
# This will fail because CSV converts everything to strings
# assert loaded_data[0]["age"] == 30 # This will be "30"
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 reveal CSV serialization issues."""
@pytest.mark.skip(reason="CSV backend doesn't support nested dictionaries")
def test_nested_dictionaries(self, backend):
"""Test nested dictionary serialization - THIS SHOULD FAIL."""
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")
# This will fail - nested dicts become string representations
assert loaded_data[0]["metadata"] == {
"score": 0.85,
"tags": ["test", "important"],
}
# Show what actually happens
print(f"Original: {data[0]['metadata']}")
print(f"Loaded: {loaded_data[0]['metadata']}")
print(f"Type: {type(loaded_data[0]['metadata'])}")
@pytest.mark.skip(reason="CSV backend doesn't support lists of objects")
def test_lists_of_objects(self, backend):
"""Test lists of objects serialization - THIS SHOULD FAIL."""
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")
# This will fail - lists become string representations
assert loaded_data[0]["results"][0]["metric"] == "accuracy"
# Show what actually happens
print(f"Original: {data[0]['results']}")
print(f"Loaded: {loaded_data[0]['results']}")
print(f"Type: {type(loaded_data[0]['results'])}")
@pytest.mark.skip(reason="CSV backend doesn't preserve data types")
def test_mixed_types(self, backend):
"""Test mixed data types - THIS WILL PARTIALLY FAIL."""
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")
# All values become strings in CSV - these assertions should fail
assert loaded_data[0]["str_field"] == "text" # This works
assert loaded_data[0]["int_field"] == 42 # This will fail - it's "42" not 42
assert (
loaded_data[0]["float_field"] == 3.14
) # This will fail - it's "3.14" not 3.14
assert (
loaded_data[0]["bool_field"] is True
) # This will fail - it's "True" not True
@pytest.mark.skip(reason="CSV backend doesn't support datetime objects")
def test_datetime_objects(self, backend):
"""Test datetime serialization - THIS SHOULD FAIL."""
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")
# Datetime objects become string representations - this should fail
original_dt = data[0]["created_at"]
loaded_dt = loaded_data[0]["created_at"]
assert isinstance(original_dt, datetime)
assert isinstance(loaded_dt, datetime) # This will fail - it's a string now!
@pytest.mark.skip(reason="CSV backend doesn't support complex nested structures")
def test_complex_nested_structure(self, backend):
"""Test deeply nested structures - THIS SHOULD FAIL BADLY."""
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")
# This will fail - complex nested structure becomes string
assert loaded_data[0]["config"]["database"]["host"] == "localhost"
# Show the mangled data
print(f"Original: {data[0]['config']}")
print(f"Loaded: {loaded_data[0]['config']}")
# 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")
# Try to create BaseModel instances - this will partially fail
try:
models = [SimpleTestModel(**item) for item in loaded_data]
print("BaseModel creation succeeded!")
print(f"First model: {models[0]}")
except Exception as e:
print(f"BaseModel creation failed: {e}")
print(
f"Loaded data types: {[(k, type(v)) for k, v in loaded_data[0].items()]}"
)
@pytest.mark.skip(reason="CSV backend doesn't support complex BaseModel validation")
def test_complex_basemodel_roundtrip(self, backend, complex_data):
"""Test BaseModel with complex data - THIS SHOULD FAIL."""
# 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")
# This will fail because nested structures are corrupted
with pytest.raises(ValidationError):
[ComplexTestModel(**item) for item in loaded_data]
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")
# Pydantic should be able to handle some string-to-type conversions
# This might work for simple types
model = SimpleTestModel(**loaded_data[0])
print(f"Type coercion successful: {model}")
assert model.age == 30 # String "30" -> int 30
assert model.score == 85.5 # String "85.5" -> float 85.5
# 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
assert loaded_data[0]["name"] == "José María"
assert loaded_data[0]["chinese"] == "你好世界"
assert "🚀" in loaded_data[0]["description"]
def test_csv_injection_protection(self, backend):
"""Test protection against CSV injection attacks."""
# CSV injection attempts
data = [
{
"formula": "=SUM(A1:A10)",
"command": "@SUM(A1:A10)",
"plus_formula": "+SUM(A1:A10)",
"minus_formula": "-SUM(A1:A10)",
}
]
backend.save_dataset("injection_test", data)
loaded_data = backend.load_dataset("injection_test")
# Data should be preserved as-is (strings)
assert loaded_data[0]["formula"] == "=SUM(A1:A10)"
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")
# Show how null values are handled
print(f"Original null: {data[0]['null_value']}")
print(f"Loaded null: {loaded_data[0]['null_value']}")
print(f"Loaded empty: '{loaded_data[0]['empty_string']}'")
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
assert len(loaded_data[0]["large_field"]) == 10000
assert loaded_data[0]["large_field"] == large_text
def test_malformed_csv_handling(self, backend, temp_dir):
"""Test behavior with malformed CSV files."""
# Create a malformed CSV file manually
malformed_csv = Path(temp_dir) / "datasets" / "malformed.csv"
malformed_csv.parent.mkdir(parents=True, exist_ok=True)
with open(malformed_csv, "w") as f:
f.write("header1,header2\n")
f.write("value1,value2,extra_value\n") # Too many columns
f.write("value3\n") # Too few columns
# Try to load malformed CSV
try:
loaded_data = backend.load_dataset("malformed")
print(f"Malformed CSV loaded: {loaded_data}")
except Exception as e:
print(f"Malformed CSV failed to load: {e}")