Re-structure the project.

This commit is contained in:
Yaojia Wang
2026-01-25 15:21:11 +01:00
parent 8fd61ea928
commit e599424a92
80 changed files with 10672 additions and 1584 deletions

204
tests/test_exceptions.py Normal file
View 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__)