""" 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 shared.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__)