205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
"""
|
|
Tests for custom exceptions.
|
|
"""
|
|
|
|
import pytest
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
from src.exceptions import (
|
|
InvoiceExtractionError,
|
|
PDFProcessingError,
|
|
OCRError,
|
|
ModelInferenceError,
|
|
FieldValidationError,
|
|
DatabaseError,
|
|
ConfigurationError,
|
|
PaymentLineParseError,
|
|
CustomerNumberParseError,
|
|
)
|
|
|
|
|
|
class TestExceptionHierarchy:
|
|
"""Test exception inheritance and hierarchy."""
|
|
|
|
def test_all_exceptions_inherit_from_base(self):
|
|
"""Test that all custom exceptions inherit from InvoiceExtractionError."""
|
|
exceptions = [
|
|
PDFProcessingError,
|
|
OCRError,
|
|
ModelInferenceError,
|
|
FieldValidationError,
|
|
DatabaseError,
|
|
ConfigurationError,
|
|
PaymentLineParseError,
|
|
CustomerNumberParseError,
|
|
]
|
|
|
|
for exc_class in exceptions:
|
|
assert issubclass(exc_class, InvoiceExtractionError)
|
|
assert issubclass(exc_class, Exception)
|
|
|
|
def test_base_exception_with_message(self):
|
|
"""Test base exception with simple message."""
|
|
error = InvoiceExtractionError("Something went wrong")
|
|
assert str(error) == "Something went wrong"
|
|
assert error.message == "Something went wrong"
|
|
assert error.details == {}
|
|
|
|
def test_base_exception_with_details(self):
|
|
"""Test base exception with additional details."""
|
|
error = InvoiceExtractionError(
|
|
"Processing failed",
|
|
details={"doc_id": "123", "page": 1}
|
|
)
|
|
assert "Processing failed" in str(error)
|
|
assert "doc_id=123" in str(error)
|
|
assert "page=1" in str(error)
|
|
assert error.details["doc_id"] == "123"
|
|
|
|
|
|
class TestSpecificExceptions:
|
|
"""Test specific exception types."""
|
|
|
|
def test_pdf_processing_error(self):
|
|
"""Test PDFProcessingError."""
|
|
error = PDFProcessingError("Failed to convert PDF", {"path": "/tmp/test.pdf"})
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "Failed to convert PDF" in str(error)
|
|
|
|
def test_ocr_error(self):
|
|
"""Test OCRError."""
|
|
error = OCRError("OCR engine failed", {"engine": "PaddleOCR"})
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "OCR engine failed" in str(error)
|
|
|
|
def test_model_inference_error(self):
|
|
"""Test ModelInferenceError."""
|
|
error = ModelInferenceError("YOLO detection failed")
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "YOLO detection failed" in str(error)
|
|
|
|
def test_field_validation_error(self):
|
|
"""Test FieldValidationError with specific attributes."""
|
|
error = FieldValidationError(
|
|
field_name="amount",
|
|
value="invalid",
|
|
reason="Not a valid number"
|
|
)
|
|
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert error.field_name == "amount"
|
|
assert error.value == "invalid"
|
|
assert error.reason == "Not a valid number"
|
|
assert "amount" in str(error)
|
|
assert "validation failed" in str(error)
|
|
|
|
def test_database_error(self):
|
|
"""Test DatabaseError."""
|
|
error = DatabaseError("Connection failed", {"host": "localhost"})
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "Connection failed" in str(error)
|
|
|
|
def test_configuration_error(self):
|
|
"""Test ConfigurationError."""
|
|
error = ConfigurationError("Missing required config")
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "Missing required config" in str(error)
|
|
|
|
def test_payment_line_parse_error(self):
|
|
"""Test PaymentLineParseError."""
|
|
error = PaymentLineParseError(
|
|
"Invalid format",
|
|
{"text": "# 123 # invalid"}
|
|
)
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "Invalid format" in str(error)
|
|
|
|
def test_customer_number_parse_error(self):
|
|
"""Test CustomerNumberParseError."""
|
|
error = CustomerNumberParseError(
|
|
"No pattern matched",
|
|
{"text": "ABC 123"}
|
|
)
|
|
assert isinstance(error, InvoiceExtractionError)
|
|
assert "No pattern matched" in str(error)
|
|
|
|
|
|
class TestExceptionCatching:
|
|
"""Test exception catching in try/except blocks."""
|
|
|
|
def test_catch_specific_exception(self):
|
|
"""Test catching specific exception type."""
|
|
with pytest.raises(PDFProcessingError):
|
|
raise PDFProcessingError("Test error")
|
|
|
|
def test_catch_base_exception(self):
|
|
"""Test catching via base class."""
|
|
with pytest.raises(InvoiceExtractionError):
|
|
raise PDFProcessingError("Test error")
|
|
|
|
def test_catch_multiple_exceptions(self):
|
|
"""Test catching multiple exception types."""
|
|
def risky_operation(error_type: str):
|
|
if error_type == "pdf":
|
|
raise PDFProcessingError("PDF error")
|
|
elif error_type == "ocr":
|
|
raise OCRError("OCR error")
|
|
else:
|
|
raise ValueError("Unknown error")
|
|
|
|
# Catch specific exceptions
|
|
with pytest.raises((PDFProcessingError, OCRError)):
|
|
risky_operation("pdf")
|
|
|
|
with pytest.raises((PDFProcessingError, OCRError)):
|
|
risky_operation("ocr")
|
|
|
|
# Different exception should not be caught
|
|
with pytest.raises(ValueError):
|
|
risky_operation("other")
|
|
|
|
def test_exception_details_preserved(self):
|
|
"""Test that exception details are preserved when caught."""
|
|
try:
|
|
raise FieldValidationError(
|
|
field_name="test_field",
|
|
value="bad_value",
|
|
reason="Test reason",
|
|
details={"extra": "info"}
|
|
)
|
|
except FieldValidationError as e:
|
|
assert e.field_name == "test_field"
|
|
assert e.value == "bad_value"
|
|
assert e.reason == "Test reason"
|
|
assert e.details["extra"] == "info"
|
|
|
|
|
|
class TestExceptionReraising:
|
|
"""Test exception re-raising patterns."""
|
|
|
|
def test_reraise_as_different_exception(self):
|
|
"""Test converting one exception type to another."""
|
|
def low_level_operation():
|
|
raise ValueError("Low-level error")
|
|
|
|
def high_level_operation():
|
|
try:
|
|
low_level_operation()
|
|
except ValueError as e:
|
|
raise PDFProcessingError(
|
|
f"High-level error: {e}",
|
|
details={"original_error": str(e)}
|
|
) from e
|
|
|
|
with pytest.raises(PDFProcessingError) as exc_info:
|
|
high_level_operation()
|
|
|
|
# Verify exception chain is preserved
|
|
assert exc_info.value.__cause__.__class__ == ValueError
|
|
assert "Low-level error" in str(exc_info.value.__cause__)
|