Re-structure the project.
This commit is contained in:
204
tests/test_exceptions.py
Normal file
204
tests/test_exceptions.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
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__)
|
||||
Reference in New Issue
Block a user