File size: 10,938 Bytes
ce4bc73 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
"""
Tests for validation utilities.
"""
import pandas as pd
import pytest
from src.folio.exceptions import DataError
from src.folio.validation import (
clean_numeric_value,
extract_option_data,
validate_dataframe,
validate_option_data,
)
class TestValidateOptionData:
"""Tests for validate_option_data function."""
def test_valid_option_data(self):
"""Test with valid option data."""
option_row = pd.Series(
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Quantity": "2",
"Last Price": "5.00",
}
)
description, quantity, price = validate_option_data(option_row)
assert description == "SPY JUN 15 2025 $100 CALL"
assert quantity == 2
assert price == 5.0
def test_missing_description(self):
"""Test with missing description."""
option_row = pd.Series(
{
"Description": None,
"Quantity": "2",
"Last Price": "5.00",
}
)
with pytest.raises(DataError, match="Missing description"):
validate_option_data(option_row)
def test_missing_quantity(self):
"""Test with missing quantity."""
option_row = pd.Series(
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Quantity": None,
"Last Price": "5.00",
}
)
with pytest.raises(DataError, match="Missing quantity"):
validate_option_data(option_row)
def test_invalid_quantity(self):
"""Test with invalid quantity format."""
option_row = pd.Series(
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Quantity": "not a number",
"Last Price": "5.00",
}
)
with pytest.raises(DataError, match="Invalid quantity format"):
validate_option_data(option_row)
def test_missing_price(self):
"""Test with missing price."""
option_row = pd.Series(
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Quantity": "2",
"Last Price": None,
}
)
with pytest.raises(DataError, match="Missing price"):
validate_option_data(option_row)
def test_invalid_price(self):
"""Test with invalid price format."""
option_row = pd.Series(
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Quantity": "2",
"Last Price": "not a price",
}
)
with pytest.raises(DataError, match="Invalid price format"):
validate_option_data(option_row)
def test_custom_field_names(self):
"""Test with custom field names."""
option_row = pd.Series(
{
"OptionDesc": "SPY JUN 15 2025 $100 CALL",
"OptionQty": "2",
"OptionPrice": "5.00",
}
)
description, quantity, price = validate_option_data(
option_row,
description_field="OptionDesc",
quantity_field="OptionQty",
price_field="OptionPrice",
)
assert description == "SPY JUN 15 2025 $100 CALL"
assert quantity == 2
assert price == 5.0
class TestExtractOptionData:
"""Tests for extract_option_data function."""
def test_extract_valid_options(self):
"""Test extracting valid options."""
option_df = pd.DataFrame(
[
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Symbol": "-SPY",
"Quantity": "2",
"Last Price": "5.00",
"Current Value": "1000.00",
},
{
"Description": "SPY JUN 15 2025 $110 CALL",
"Symbol": "-SPY",
"Quantity": "-1",
"Last Price": "2.00",
"Current Value": "-200.00",
},
]
)
options_data = extract_option_data(option_df)
assert len(options_data) == 2
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"
assert options_data[0]["quantity"] == 2
assert options_data[0]["price"] == 5.0
assert options_data[0]["symbol"] == "-SPY"
assert "row_index" in options_data[0]
assert options_data[1]["description"] == "SPY JUN 15 2025 $110 CALL"
assert options_data[1]["quantity"] == -1
assert options_data[1]["price"] == 2.0
def test_extract_with_filter(self):
"""Test extracting options with a filter function."""
option_df = pd.DataFrame(
[
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Symbol": "-SPY",
"Quantity": "2",
"Last Price": "5.00",
},
{
"Description": "AAPL JUN 15 2025 $200 CALL",
"Symbol": "-AAPL",
"Quantity": "1",
"Last Price": "10.00",
},
]
)
# Filter for SPY options only
options_data = extract_option_data(
option_df,
filter_func=lambda row: row["Symbol"] == "-SPY",
)
assert len(options_data) == 1
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"
assert options_data[0]["symbol"] == "-SPY"
def test_extract_with_invalid_options(self):
"""Test extracting options with some invalid data."""
option_df = pd.DataFrame(
[
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Symbol": "-SPY",
"Quantity": "2",
"Last Price": "5.00",
},
{
"Description": None, # Invalid: missing description
"Symbol": "-AAPL",
"Quantity": "1",
"Last Price": "10.00",
},
{
"Description": "AAPL JUN 15 2025 $200 CALL",
"Symbol": "-AAPL",
"Quantity": "not a number", # Invalid: bad quantity
"Last Price": "10.00",
},
]
)
options_data = extract_option_data(option_df)
# Only the first option should be extracted
assert len(options_data) == 1
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"
def test_extract_without_row_index(self):
"""Test extracting options without including row index."""
option_df = pd.DataFrame(
[
{
"Description": "SPY JUN 15 2025 $100 CALL",
"Symbol": "-SPY",
"Quantity": "2",
"Last Price": "5.00",
},
]
)
options_data = extract_option_data(option_df, include_row_index=False)
assert len(options_data) == 1
assert "row_index" not in options_data[0]
class TestValidateDataframe:
"""Tests for validate_dataframe function."""
def test_valid_dataframe(self):
"""Test with a valid DataFrame."""
df = pd.DataFrame(
{
"Symbol": ["SPY", "AAPL"],
"Quantity": [10, 20],
"Price": [100.0, 200.0],
}
)
result = validate_dataframe(df, ["Symbol", "Quantity", "Price"])
# Should return the original DataFrame
assert result is df
def test_none_dataframe(self):
"""Test with None DataFrame."""
with pytest.raises(DataError, match="dataframe is None"):
validate_dataframe(None, ["Symbol"])
def test_empty_dataframe(self):
"""Test with empty DataFrame."""
df = pd.DataFrame()
with pytest.raises(DataError, match="dataframe is empty"):
validate_dataframe(df, ["Symbol"])
def test_missing_columns(self):
"""Test with missing required columns."""
df = pd.DataFrame(
{
"Symbol": ["SPY", "AAPL"],
"Quantity": [10, 20],
}
)
with pytest.raises(DataError, match="missing required columns: Price"):
validate_dataframe(df, ["Symbol", "Quantity", "Price"])
def test_custom_name(self):
"""Test with custom DataFrame name."""
with pytest.raises(DataError, match="portfolio is None"):
validate_dataframe(None, ["Symbol"], name="portfolio")
class TestCleanNumericValue:
"""Tests for clean_numeric_value function."""
def test_valid_numeric(self):
"""Test with valid numeric values."""
assert clean_numeric_value(10) == 10.0
assert clean_numeric_value(10.5) == 10.5
assert clean_numeric_value("10") == 10.0
assert clean_numeric_value("10.5") == 10.5
def test_currency_format(self):
"""Test with currency formatted values."""
assert clean_numeric_value("$10.50") == 10.5
assert clean_numeric_value("$1,234.56") == 1234.56
assert clean_numeric_value("($10.50)") == -10.5 # Parentheses for negative
def test_none_value(self):
"""Test with None value."""
with pytest.raises(ValueError, match="Value is NaN or None"):
clean_numeric_value(None)
# With default
assert clean_numeric_value(None, default=0) == 0
def test_nan_value(self):
"""Test with NaN value."""
with pytest.raises(ValueError, match="Value is NaN or None"):
clean_numeric_value(float("nan"))
# With default
assert clean_numeric_value(float("nan"), default=0) == 0
def test_invalid_format(self):
"""Test with invalid format."""
with pytest.raises(ValueError, match="Could not convert"):
clean_numeric_value("not a number")
# With default
assert clean_numeric_value("not a number", default=0) == 0
def test_zero_constraint(self):
"""Test with zero constraint."""
with pytest.raises(ValueError, match="Zero value not allowed"):
clean_numeric_value(0, allow_zero=False)
# With default
assert clean_numeric_value(0, allow_zero=False, default=1) == 1
def test_negative_constraint(self):
"""Test with negative constraint."""
with pytest.raises(ValueError, match="Negative value not allowed"):
clean_numeric_value(-10, allow_negative=False)
# With default
assert clean_numeric_value(-10, allow_negative=False, default=10) == 10
|