test: add monthly limit, counter, and cloud-primary engine tests (refs #127)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 8m46s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 22s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 8m46s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 22s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- Update existing hybrid engine tests for new Redis counter behavior - Add cloud-primary path tests (under/at limit, fallback, errors) - Add Redis counter increment and TTL verification tests - Add Redis failure graceful handling test - Update cloud engine error message assertion for WIF config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -355,7 +355,7 @@ class TestCloudEngine:
|
|||||||
from app.engines.cloud_engine import CloudEngine
|
from app.engines.cloud_engine import CloudEngine
|
||||||
|
|
||||||
engine = CloudEngine(key_path="/nonexistent/key.json")
|
engine = CloudEngine(key_path="/nonexistent/key.json")
|
||||||
with pytest.raises(EngineUnavailableError, match="key not found"):
|
with pytest.raises(EngineUnavailableError, match="credential config not found"):
|
||||||
engine._get_client()
|
engine._get_client()
|
||||||
|
|
||||||
@patch("os.path.isfile", return_value=True)
|
@patch("os.path.isfile", return_value=True)
|
||||||
@@ -414,6 +414,16 @@ class TestCloudEngine:
|
|||||||
|
|
||||||
|
|
||||||
class TestHybridEngine:
|
class TestHybridEngine:
|
||||||
|
"""Tests for HybridEngine with monthly Vision API cap."""
|
||||||
|
|
||||||
|
def _mock_redis(self, current_count: int = 0) -> MagicMock:
|
||||||
|
"""Create a mock Redis instance with a configurable counter value."""
|
||||||
|
mock_r = MagicMock()
|
||||||
|
mock_r.get.return_value = str(current_count) if current_count else None
|
||||||
|
mock_pipe = MagicMock()
|
||||||
|
mock_r.pipeline.return_value = mock_pipe
|
||||||
|
return mock_r
|
||||||
|
|
||||||
def test_name_with_fallback(self) -> None:
|
def test_name_with_fallback(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
@@ -432,13 +442,15 @@ class TestHybridEngine:
|
|||||||
engine = HybridEngine(primary=primary)
|
engine = HybridEngine(primary=primary)
|
||||||
assert engine.name == "hybrid(paddleocr+none)"
|
assert engine.name == "hybrid(paddleocr+none)"
|
||||||
|
|
||||||
|
# --- Local-primary path (original confidence-based fallback) ---
|
||||||
|
|
||||||
def test_high_confidence_skips_fallback(self) -> None:
|
def test_high_confidence_skips_fallback(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
primary = MagicMock(spec=OcrEngine)
|
||||||
fallback = MagicMock(spec=OcrEngine)
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
primary.name = "paddleocr"
|
primary.name = "paddleocr"
|
||||||
fallback.name = "cloud"
|
fallback.name = "tesseract"
|
||||||
primary.recognize.return_value = _make_result("VIN123", 0.95, "paddleocr")
|
primary.recognize.return_value = _make_result("VIN123", 0.95, "paddleocr")
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
@@ -447,22 +459,6 @@ class TestHybridEngine:
|
|||||||
assert result.engine_name == "paddleocr"
|
assert result.engine_name == "paddleocr"
|
||||||
fallback.recognize.assert_not_called()
|
fallback.recognize.assert_not_called()
|
||||||
|
|
||||||
def test_low_confidence_triggers_fallback(self) -> None:
|
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
|
||||||
fallback = MagicMock(spec=OcrEngine)
|
|
||||||
primary.name = "paddleocr"
|
|
||||||
fallback.name = "google_vision"
|
|
||||||
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
|
||||||
fallback.recognize.return_value = _make_result("VIN456", 0.92, "google_vision")
|
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
|
||||||
assert result.text == "VIN456"
|
|
||||||
assert result.engine_name == "google_vision"
|
|
||||||
fallback.recognize.assert_called_once()
|
|
||||||
|
|
||||||
def test_low_confidence_no_fallback_returns_primary(self) -> None:
|
def test_low_confidence_no_fallback_returns_primary(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
@@ -474,6 +470,57 @@ class TestHybridEngine:
|
|||||||
result = engine.recognize(b"img", OcrConfig())
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
assert result.text == "VIN123"
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
|
def test_exact_threshold_skips_fallback(self) -> None:
|
||||||
|
"""When confidence == threshold, no fallback needed (>= check)."""
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "paddleocr"
|
||||||
|
fallback.name = "tesseract"
|
||||||
|
primary.recognize.return_value = _make_result("VIN", 0.6, "paddleocr")
|
||||||
|
|
||||||
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.engine_name == "paddleocr"
|
||||||
|
fallback.recognize.assert_not_called()
|
||||||
|
|
||||||
|
# --- Local-primary with cloud fallback (subject to monthly cap) ---
|
||||||
|
|
||||||
|
def test_low_confidence_triggers_cloud_fallback(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "paddleocr"
|
||||||
|
fallback.name = "google_vision"
|
||||||
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
|
fallback.recognize.return_value = _make_result("VIN456", 0.92, "google_vision")
|
||||||
|
|
||||||
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
engine._redis = self._mock_redis(current_count=0)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN456"
|
||||||
|
assert result.engine_name == "google_vision"
|
||||||
|
|
||||||
|
def test_cloud_fallback_skipped_when_limit_reached(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "paddleocr"
|
||||||
|
fallback.name = "google_vision"
|
||||||
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
|
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=100
|
||||||
|
)
|
||||||
|
engine._redis = self._mock_redis(current_count=100)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN123"
|
||||||
|
assert result.engine_name == "paddleocr"
|
||||||
|
fallback.recognize.assert_not_called()
|
||||||
|
|
||||||
def test_fallback_lower_confidence_returns_primary(self) -> None:
|
def test_fallback_lower_confidence_returns_primary(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
@@ -485,10 +532,11 @@ class TestHybridEngine:
|
|||||||
fallback.recognize.return_value = _make_result("VIN456", 0.3, "google_vision")
|
fallback.recognize.return_value = _make_result("VIN456", 0.3, "google_vision")
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
engine._redis = self._mock_redis(current_count=0)
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
assert result.text == "VIN123"
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
def test_fallback_engine_error_returns_primary(self) -> None:
|
def test_cloud_fallback_error_returns_primary(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
primary = MagicMock(spec=OcrEngine)
|
||||||
@@ -499,25 +547,14 @@ class TestHybridEngine:
|
|||||||
fallback.recognize.side_effect = EngineUnavailableError("key missing")
|
fallback.recognize.side_effect = EngineUnavailableError("key missing")
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
engine._redis = self._mock_redis(current_count=0)
|
||||||
assert result.text == "VIN123"
|
|
||||||
|
|
||||||
def test_fallback_unexpected_error_returns_primary(self) -> None:
|
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
|
||||||
fallback = MagicMock(spec=OcrEngine)
|
|
||||||
primary.name = "paddleocr"
|
|
||||||
fallback.name = "google_vision"
|
|
||||||
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
|
||||||
fallback.recognize.side_effect = RuntimeError("network error")
|
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
assert result.text == "VIN123"
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
@patch("app.engines.hybrid_engine.time")
|
@patch("app.engines.hybrid_engine.time")
|
||||||
def test_fallback_timeout_returns_primary(self, mock_time: MagicMock) -> None:
|
def test_cloud_fallback_timeout_returns_primary(
|
||||||
|
self, mock_time: MagicMock
|
||||||
|
) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
primary = MagicMock(spec=OcrEngine)
|
||||||
@@ -525,28 +562,211 @@ class TestHybridEngine:
|
|||||||
primary.name = "paddleocr"
|
primary.name = "paddleocr"
|
||||||
fallback.name = "google_vision"
|
fallback.name = "google_vision"
|
||||||
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
fallback.recognize.return_value = _make_result("VIN456", 0.92, "google_vision")
|
fallback.recognize.return_value = _make_result(
|
||||||
# Simulate 6-second delay (exceeds 5s limit)
|
"VIN456", 0.92, "google_vision"
|
||||||
|
)
|
||||||
mock_time.monotonic.side_effect = [0.0, 6.0]
|
mock_time.monotonic.side_effect = [0.0, 6.0]
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
engine._redis = self._mock_redis(current_count=0)
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
assert result.text == "VIN123" # timeout -> use primary
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
def test_exact_threshold_skips_fallback(self) -> None:
|
# --- Cloud-primary path ---
|
||||||
"""When confidence == threshold, no fallback needed (>= check)."""
|
|
||||||
|
def test_cloud_primary_returns_result_when_under_limit(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
primary.recognize.return_value = _make_result(
|
||||||
|
"VIN789", 0.95, "google_vision"
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = self._mock_redis(current_count=500)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN789"
|
||||||
|
assert result.engine_name == "google_vision"
|
||||||
|
fallback.recognize.assert_not_called()
|
||||||
|
|
||||||
|
def test_cloud_primary_falls_back_when_limit_reached(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
fallback.recognize.return_value = _make_result(
|
||||||
|
"VIN_LOCAL", 0.75, "paddleocr"
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = self._mock_redis(current_count=1000)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN_LOCAL"
|
||||||
|
assert result.engine_name == "paddleocr"
|
||||||
|
primary.recognize.assert_not_called()
|
||||||
|
|
||||||
|
def test_cloud_primary_no_fallback_raises_when_limit_reached(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=None, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = self._mock_redis(current_count=1000)
|
||||||
|
with pytest.raises(EngineProcessingError, match="no fallback"):
|
||||||
|
engine.recognize(b"img", OcrConfig())
|
||||||
|
|
||||||
|
def test_cloud_primary_error_falls_back(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
primary.recognize.side_effect = EngineUnavailableError("API down")
|
||||||
|
fallback.recognize.return_value = _make_result(
|
||||||
|
"VIN_LOCAL", 0.75, "paddleocr"
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = self._mock_redis(current_count=500)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN_LOCAL"
|
||||||
|
assert result.engine_name == "paddleocr"
|
||||||
|
|
||||||
|
# --- Redis counter behavior ---
|
||||||
|
|
||||||
|
def test_counter_increments_on_successful_cloud_call(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
primary.recognize.return_value = _make_result(
|
||||||
|
"VIN789", 0.95, "google_vision"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_r = self._mock_redis(current_count=10)
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = mock_r
|
||||||
|
engine.recognize(b"img", OcrConfig())
|
||||||
|
|
||||||
|
mock_r.pipeline.assert_called_once()
|
||||||
|
pipe = mock_r.pipeline.return_value
|
||||||
|
pipe.incr.assert_called_once()
|
||||||
|
pipe.expire.assert_called_once()
|
||||||
|
pipe.execute.assert_called_once()
|
||||||
|
|
||||||
|
def test_counter_not_incremented_when_limit_reached(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
fallback.recognize.return_value = _make_result(
|
||||||
|
"VIN_LOCAL", 0.75, "paddleocr"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_r = self._mock_redis(current_count=1000)
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = mock_r
|
||||||
|
engine.recognize(b"img", OcrConfig())
|
||||||
|
|
||||||
|
mock_r.pipeline.assert_not_called()
|
||||||
|
|
||||||
|
def test_redis_failure_assumes_limit_not_reached(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "google_vision"
|
||||||
|
fallback.name = "paddleocr"
|
||||||
|
primary.recognize.return_value = _make_result(
|
||||||
|
"VIN789", 0.95, "google_vision"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_r = MagicMock()
|
||||||
|
mock_r.get.side_effect = Exception("Redis connection refused")
|
||||||
|
mock_pipe = MagicMock()
|
||||||
|
mock_r.pipeline.return_value = mock_pipe
|
||||||
|
engine = HybridEngine(
|
||||||
|
primary=primary, fallback=fallback, threshold=0.6, monthly_limit=1000
|
||||||
|
)
|
||||||
|
engine._redis = mock_r
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN789"
|
||||||
|
|
||||||
|
# --- Non-cloud fallback path (no cap needed) ---
|
||||||
|
|
||||||
|
def test_non_cloud_fallback_not_subject_to_cap(self) -> None:
|
||||||
from app.engines.hybrid_engine import HybridEngine
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
primary = MagicMock(spec=OcrEngine)
|
primary = MagicMock(spec=OcrEngine)
|
||||||
fallback = MagicMock(spec=OcrEngine)
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
primary.name = "paddleocr"
|
primary.name = "paddleocr"
|
||||||
fallback.name = "cloud"
|
fallback.name = "tesseract"
|
||||||
primary.recognize.return_value = _make_result("VIN", 0.6, "paddleocr")
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
|
fallback.recognize.return_value = _make_result(
|
||||||
|
"VIN456", 0.92, "tesseract"
|
||||||
|
)
|
||||||
|
|
||||||
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
result = engine.recognize(b"img", OcrConfig())
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
assert result.engine_name == "paddleocr"
|
assert result.text == "VIN456"
|
||||||
fallback.recognize.assert_not_called()
|
assert result.engine_name == "tesseract"
|
||||||
|
|
||||||
|
@patch("app.engines.hybrid_engine.time")
|
||||||
|
def test_non_cloud_fallback_timeout_returns_primary(
|
||||||
|
self, mock_time: MagicMock
|
||||||
|
) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "paddleocr"
|
||||||
|
fallback.name = "tesseract"
|
||||||
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
|
fallback.recognize.return_value = _make_result(
|
||||||
|
"VIN456", 0.92, "tesseract"
|
||||||
|
)
|
||||||
|
mock_time.monotonic.side_effect = [0.0, 6.0]
|
||||||
|
|
||||||
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
|
def test_non_cloud_fallback_error_returns_primary(self) -> None:
|
||||||
|
from app.engines.hybrid_engine import HybridEngine
|
||||||
|
|
||||||
|
primary = MagicMock(spec=OcrEngine)
|
||||||
|
fallback = MagicMock(spec=OcrEngine)
|
||||||
|
primary.name = "paddleocr"
|
||||||
|
fallback.name = "tesseract"
|
||||||
|
primary.recognize.return_value = _make_result("VIN123", 0.3, "paddleocr")
|
||||||
|
fallback.recognize.side_effect = RuntimeError("crash")
|
||||||
|
|
||||||
|
engine = HybridEngine(primary=primary, fallback=fallback, threshold=0.6)
|
||||||
|
result = engine.recognize(b"img", OcrConfig())
|
||||||
|
assert result.text == "VIN123"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -599,6 +819,7 @@ class TestEngineFactory:
|
|||||||
mock_settings.ocr_primary_engine = "paddleocr"
|
mock_settings.ocr_primary_engine = "paddleocr"
|
||||||
mock_settings.ocr_fallback_engine = "google_vision"
|
mock_settings.ocr_fallback_engine = "google_vision"
|
||||||
mock_settings.ocr_fallback_threshold = 0.7
|
mock_settings.ocr_fallback_threshold = 0.7
|
||||||
|
mock_settings.vision_monthly_limit = 1000
|
||||||
mock_primary = MagicMock(spec=OcrEngine)
|
mock_primary = MagicMock(spec=OcrEngine)
|
||||||
mock_fallback = MagicMock(spec=OcrEngine)
|
mock_fallback = MagicMock(spec=OcrEngine)
|
||||||
mock_create.side_effect = [mock_primary, mock_fallback]
|
mock_create.side_effect = [mock_primary, mock_fallback]
|
||||||
|
|||||||
Reference in New Issue
Block a user