|
@@ -459,6 +459,138 @@ class TestCameraAPI:
|
|
|
assert result["success"] is True
|
|
assert result["success"] is True
|
|
|
assert "index" in result
|
|
assert "index" in result
|
|
|
|
|
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+ # Regression: #1359 — the manual UI check/calibrate routes must derive
|
|
|
|
|
+ # use_external from the printer's external_camera_enabled setting when
|
|
|
|
|
+ # the caller omits the flag. Otherwise the UI calibrates against the
|
|
|
|
|
+ # built-in camera while the runtime auto-check at print start uses the
|
|
|
|
|
+ # external one, producing a permanent "build plate not empty".
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_check_plate_defaults_use_external_when_external_camera_enabled(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Omitting use_external on a printer with external camera enabled
|
|
|
|
|
+ must call the service with use_external=True."""
|
|
|
|
|
+ printer = await printer_factory(
|
|
|
|
|
+ external_camera_enabled=True,
|
|
|
|
|
+ external_camera_url="http://192.168.1.50/mjpeg",
|
|
|
|
|
+ external_camera_type="mjpeg",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ mock_result = MagicMock()
|
|
|
|
|
+ mock_result.to_dict.return_value = {
|
|
|
|
|
+ "is_empty": True,
|
|
|
|
|
+ "confidence": 0.95,
|
|
|
|
|
+ "difference_percent": 0.5,
|
|
|
|
|
+ "message": "Plate appears empty",
|
|
|
|
|
+ "has_debug_image": False,
|
|
|
|
|
+ "needs_calibration": False,
|
|
|
|
|
+ }
|
|
|
|
|
+ mock_result.debug_image = None
|
|
|
|
|
+
|
|
|
|
|
+ mock_detector = MagicMock()
|
|
|
|
|
+ mock_detector.get_calibration_count.return_value = 0
|
|
|
|
|
+ mock_detector.MAX_REFERENCES = 5
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
|
|
|
|
|
+ patch("backend.app.services.plate_detection.check_plate_empty", new_callable=AsyncMock) as mock_check,
|
|
|
|
|
+ patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_check.return_value = mock_result
|
|
|
|
|
+ response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/check-plate")
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ assert mock_check.await_args.kwargs["use_external"] is True
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_check_plate_defaults_use_external_false_when_external_camera_disabled(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Omitting use_external on a printer without an external camera
|
|
|
|
|
+ must call the service with use_external=False (built-in)."""
|
|
|
|
|
+ printer = await printer_factory() # external_camera_enabled defaults to False
|
|
|
|
|
+
|
|
|
|
|
+ mock_result = MagicMock()
|
|
|
|
|
+ mock_result.to_dict.return_value = {
|
|
|
|
|
+ "is_empty": True,
|
|
|
|
|
+ "confidence": 0.95,
|
|
|
|
|
+ "difference_percent": 0.5,
|
|
|
|
|
+ "message": "Plate appears empty",
|
|
|
|
|
+ "has_debug_image": False,
|
|
|
|
|
+ "needs_calibration": False,
|
|
|
|
|
+ }
|
|
|
|
|
+ mock_result.debug_image = None
|
|
|
|
|
+
|
|
|
|
|
+ mock_detector = MagicMock()
|
|
|
|
|
+ mock_detector.get_calibration_count.return_value = 0
|
|
|
|
|
+ mock_detector.MAX_REFERENCES = 5
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
|
|
|
|
|
+ patch("backend.app.services.plate_detection.check_plate_empty", new_callable=AsyncMock) as mock_check,
|
|
|
|
|
+ patch("backend.app.services.plate_detection.PlateDetector", return_value=mock_detector),
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_check.return_value = mock_result
|
|
|
|
|
+ response = await async_client.get(f"/api/v1/printers/{printer.id}/camera/check-plate")
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ assert mock_check.await_args.kwargs["use_external"] is False
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_calibrate_plate_defaults_use_external_when_external_camera_enabled(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Calibrating with use_external omitted on an external-camera-enabled
|
|
|
|
|
+ printer captures the reference from the external camera — matching
|
|
|
|
|
+ what the runtime check at print start will compare against (#1359)."""
|
|
|
|
|
+ printer = await printer_factory(
|
|
|
|
|
+ external_camera_enabled=True,
|
|
|
|
|
+ external_camera_url="http://192.168.1.50/mjpeg",
|
|
|
|
|
+ external_camera_type="mjpeg",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
|
|
|
|
|
+ patch("backend.app.services.plate_detection.calibrate_plate", new_callable=AsyncMock) as mock_calibrate,
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_calibrate.return_value = (True, "Calibration saved (1/5 references)", 0)
|
|
|
|
|
+ response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate")
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ assert mock_calibrate.await_args.kwargs["use_external"] is True
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_calibrate_plate_explicit_use_external_false_overrides_default(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """An explicit use_external=false from the caller still wins even
|
|
|
|
|
+ when the printer has an external camera configured, so power users
|
|
|
|
|
+ can force a built-in-camera reference if they ever need to."""
|
|
|
|
|
+ printer = await printer_factory(
|
|
|
|
|
+ external_camera_enabled=True,
|
|
|
|
|
+ external_camera_url="http://192.168.1.50/mjpeg",
|
|
|
|
|
+ external_camera_type="mjpeg",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.services.plate_detection.is_plate_detection_available", return_value=True),
|
|
|
|
|
+ patch("backend.app.services.plate_detection.calibrate_plate", new_callable=AsyncMock) as mock_calibrate,
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_calibrate.return_value = (True, "Calibration saved (1/5 references)", 0)
|
|
|
|
|
+ response = await async_client.post(
|
|
|
|
|
+ f"/api/v1/printers/{printer.id}/camera/plate-detection/calibrate?use_external=false"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ assert mock_calibrate.await_args.kwargs["use_external"] is False
|
|
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_delete_calibration_printer_not_found(self, async_client: AsyncClient):
|
|
async def test_delete_calibration_printer_not_found(self, async_client: AsyncClient):
|