"""gitea:Adriano/TieMeasureFlow#97 - operator: image already analysed on entry. The backend (POST /api/vision/check, task.acquisition_json) is built elsewhere: api_client and requests are mocked here with the contract's shapes. """ import io import json from unittest.mock import MagicMock, patch from tests.conftest import api_get_router from tests.test_measure_camera_grab import ( PNG, RECIPE, STATION, TASK, _fake, _station, ) ACQ = {"camera_url": "http://acq-host:9000", "camera_source": "camera:601020", "modo": "in_linea"} def _task_with(acq): return {**TASK, "acquisition_json": acq} # --- grab: the camera of the task, else the station's ------------------------- def test_grab_with_task_id_uses_task_camera(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) mock_api_client.get.side_effect = api_get_router({"/api/tasks/12": _task_with(ACQ)}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", data={"task_id": "12"}) assert resp.status_code == 200 and resp.data == PNG args, kwargs = post.call_args assert args[0] == "http://acq-host:9000/camera/grab" assert kwargs["json"]["source"] == "camera:601020" mock_api_client.get_station_by_code.assert_not_called() def test_grab_task_id_in_json_works_too(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) mock_api_client.get.side_effect = api_get_router({"/api/tasks/12": _task_with(ACQ)}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", json={"task_id": 12}) assert resp.status_code == 200 assert post.call_args[0][0] == "http://acq-host:9000/camera/grab" def test_grab_task_without_acquisition_uses_station(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) mock_api_client.get.side_effect = api_get_router({"/api/tasks/12": _task_with(None)}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", data={"task_id": "12"}) assert resp.status_code == 200 args, kwargs = post.call_args assert args[0] == "http://host.docker.internal:8765/camera/grab" assert kwargs["json"]["source"] == "camera:folder" def test_grab_task_camera_wins_even_if_station_has_none(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, {**STATION, "camera_url": None}) mock_api_client.get.side_effect = api_get_router({"/api/tasks/12": _task_with(ACQ)}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", data={"task_id": "12"}) assert resp.status_code == 200 assert post.call_args[0][0] == "http://acq-host:9000/camera/grab" def test_grab_task_lookup_error_falls_back_to_station(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) mock_api_client.get.side_effect = api_get_router( {"/api/tasks/12": {"error": True, "status_code": 404, "detail": "x"}}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", data={"task_id": "12"}) assert resp.status_code == 200 assert post.call_args[0][0] == "http://host.docker.internal:8765/camera/grab" def test_grab_acquisition_as_json_string(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, {**STATION, "camera_url": None}) mock_api_client.get.side_effect = api_get_router( {"/api/tasks/12": _task_with(json.dumps(ACQ))}) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab", data={"task_id": "12"}) assert resp.status_code == 200 assert post.call_args[0][0] == "http://acq-host:9000/camera/grab" # --- check proxy --------------------------------------------------------------- CHECK_OK = { "outcome": "ok", "overlay": [], "failures": [], "calibration": "ok", "measurements": [{"subtask_id": 1, "value": 9.9, "pass_fail": "pass"}], } def test_check_proxy_forwards_multipart(logged_in_client, mock_api_client): mock_api_client.post.return_value = CHECK_OK resp = logged_in_client.post( "/measure/api/vision/check", data={ "task_id": "12", "production_run_id": "4", "image": (io.BytesIO(b"\xff\xd8\xff fake"), "shot.jpg", "image/jpeg"), }, content_type="multipart/form-data", ) assert resp.status_code == 200 assert resp.get_json()["measurements"][0]["pass_fail"] == "pass" args, kwargs = mock_api_client.post.call_args assert args[0] == "/api/vision/check" assert kwargs["data"] == {"task_id": "12", "production_run_id": "4"} name, content, mime = kwargs["files"]["image"] assert name == "shot.jpg" and mime == "image/jpeg" and content.startswith(b"\xff\xd8") def test_check_proxy_without_run_leaves_it_out(logged_in_client, mock_api_client): mock_api_client.post.return_value = CHECK_OK logged_in_client.post( "/measure/api/vision/check", data={"task_id": "12", "image": (io.BytesIO(b"x"), "a.jpg", "image/jpeg")}, content_type="multipart/form-data", ) assert mock_api_client.post.call_args[1]["data"] == {"task_id": "12"} def test_check_proxy_requires_image(logged_in_client, mock_api_client): resp = logged_in_client.post("/measure/api/vision/check", data={"task_id": "12"}) assert resp.status_code == 400 mock_api_client.post.assert_not_called() def test_check_proxy_relays_backend_error(logged_in_client, mock_api_client): mock_api_client.post.return_value = {"error": True, "status_code": 422, "detail": "nessun grafo"} resp = logged_in_client.post( "/measure/api/vision/check", data={"task_id": "12", "image": (io.BytesIO(b"x"), "a.jpg", "image/jpeg")}, content_type="multipart/form-data", ) assert resp.status_code == 422 assert resp.get_json()["detail"] == "nessun grafo" def test_check_requires_login(client): resp = client.post("/measure/api/vision/check") assert resp.status_code in (302, 401) # --- execute page: buttons and config ------------------------------------------- def _page(logged_in_client, mock_api_client, monkeypatch, station, task): _station(mock_api_client, monkeypatch, station) mock_api_client.get_station_recipes.return_value = {"items": [{"id": 3}]} mock_api_client.get.side_effect = api_get_router({ "/api/tasks/12": task, "/api/recipes/3/tasks": [{"id": 12, "order_index": 0}], "/api/recipes/3": RECIPE, }) return logged_in_client.get("/measure/execute/12").get_data(as_text=True) def test_page_impulsive_has_acquire_and_record(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, STATION, TASK) assert 'cameraAvailable: true' in html assert 'modo: "impulsiva"' in html assert "/measure/api/vision/check" in html assert "grab()" in html and "record()" in html assert "togglePause()" not in html assert "Anteprima, non registrata" in html def test_page_in_line_has_pause_and_record(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, STATION, _task_with(ACQ)) assert 'modo: "in_linea"' in html assert "togglePause()" in html and "record()" in html assert "grab()" not in html # the loop grabs by itself def test_page_task_camera_enables_it_without_station_camera(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, {**STATION, "camera_url": None}, _task_with({**ACQ, "modo": "impulsiva"})) assert 'cameraAvailable: true' in html assert "grab()" in html def test_page_without_camera_is_as_before(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, {**STATION, "camera_url": None}, TASK) assert 'cameraAvailable: false' in html assert "record()" not in html and "togglePause()" not in html and "grab()" not in html