192d4b590c
Route POST /measure/api/vision/grab (proxy verso il servizio camera), get_station_by_code, pulsante «Acquisisci dalla camera», grab() in vision_execute.js, campi camera_url e camera_source nell'admin stazioni, traduzioni it/en, extra_hosts host.docker.internal nel compose di sviluppo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
162 lines
6.5 KiB
Python
162 lines
6.5 KiB
Python
"""gitea:Adriano/TieMeasureFlow#72 - frame grab from the station camera.
|
|
|
|
The backend (station.camera_url / camera_source, GET /api/stations/by-code/{code})
|
|
is built elsewhere: api_client and requests are mocked here.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import requests
|
|
|
|
from tests.conftest import api_get_router
|
|
|
|
STATION = {
|
|
"id": 1, "code": "ST-TEST", "name": "Linea",
|
|
"camera_url": "http://host.docker.internal:8765",
|
|
"camera_source": "camera:folder",
|
|
}
|
|
PNG = b"\x89PNG\r\n\x1a\n fake"
|
|
|
|
|
|
def _fake(status=200, content=PNG, ctype="image/png", json_body=None):
|
|
resp = MagicMock(status_code=status, content=content, text=content.decode("latin-1"))
|
|
resp.headers = {"content-type": ctype}
|
|
if json_body is None:
|
|
resp.json.side_effect = ValueError("no json")
|
|
else:
|
|
resp.json.return_value = json_body
|
|
return resp
|
|
|
|
|
|
def _station(mock_api_client, monkeypatch, station):
|
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
|
mock_api_client.get_station_by_code.return_value = station
|
|
|
|
|
|
def test_grab_without_camera_is_409(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, {**STATION, "camera_url": None})
|
|
with patch("blueprints.measure.http_requests.post") as post:
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 409
|
|
assert resp.get_json()["error"]
|
|
post.assert_not_called()
|
|
|
|
|
|
def test_grab_without_source_is_409(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, {**STATION, "camera_source": None})
|
|
with patch("blueprints.measure.http_requests.post") as post:
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 409
|
|
post.assert_not_called()
|
|
|
|
|
|
def test_grab_station_lookup_error_is_409(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, {"error": True, "status_code": 404, "detail": "x"})
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 409
|
|
|
|
|
|
def test_grab_passes_png_through(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, STATION)
|
|
with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post:
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 200
|
|
assert resp.mimetype == "image/png"
|
|
assert resp.data == PNG
|
|
mock_api_client.get_station_by_code.assert_called_once_with("ST-TEST")
|
|
args, kwargs = post.call_args
|
|
assert args[0] == "http://host.docker.internal:8765/camera/grab"
|
|
assert kwargs["json"] == {
|
|
"source": "camera:folder", "trigger": "continuo", "timeout_ms": 5000,
|
|
}
|
|
assert kwargs["timeout"] == 15
|
|
|
|
|
|
def test_grab_service_503_is_502(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, STATION)
|
|
fake = _fake(503, b"{}", "application/json", {"detail": "camera occupata"})
|
|
with patch("blueprints.measure.http_requests.post", return_value=fake):
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 502
|
|
body = resp.get_json()
|
|
assert body["error"] is True
|
|
assert "camera occupata" in body["detail"]
|
|
|
|
|
|
def test_grab_service_504_and_404_are_502(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, STATION)
|
|
for status in (404, 504):
|
|
with patch("blueprints.measure.http_requests.post",
|
|
return_value=_fake(status, b"nope", "text/plain")):
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 502
|
|
assert resp.get_json()["error"] is True
|
|
|
|
|
|
def test_grab_connection_error_is_502(logged_in_client, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, STATION)
|
|
with patch("blueprints.measure.http_requests.post",
|
|
side_effect=requests.ConnectionError("refused")):
|
|
resp = logged_in_client.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 502
|
|
assert resp.get_json()["error"] is True
|
|
|
|
|
|
def test_grab_requires_measurement_tec(flask_app, mock_api_client, monkeypatch):
|
|
_station(mock_api_client, monkeypatch, STATION)
|
|
with flask_app.test_client() as c:
|
|
with c.session_transaction() as sess:
|
|
sess["api_key"] = "k"
|
|
sess["user"] = {"id": 2, "username": "m", "roles": ["Maker"],
|
|
"is_admin": False, "active": True}
|
|
sess["user_id"] = 2
|
|
with patch("blueprints.measure.http_requests.post") as post:
|
|
resp = c.post("/measure/api/vision/grab")
|
|
assert resp.status_code == 403
|
|
post.assert_not_called()
|
|
|
|
|
|
def test_grab_requires_login(client):
|
|
resp = client.post("/measure/api/vision/grab")
|
|
assert resp.status_code in (302, 401)
|
|
|
|
|
|
# --- execute page: button only when the station has a camera -----------------
|
|
TASK = {
|
|
"id": 12, "recipe_id": 3, "version_id": 5, "task_type": "camera_measure",
|
|
"title": "Foto", "directive": None, "description": None, "file_path": None,
|
|
"file_type": None, "annotations_json": None, "subtasks": [],
|
|
}
|
|
RECIPE = {
|
|
"id": 3, "code": "REC-1", "name": "Ricetta",
|
|
"measurement_interval_minutes": 30, "allow_manual_input": True,
|
|
"current_version": {"id": 5, "version_number": 1},
|
|
}
|
|
|
|
|
|
def _page(logged_in_client, mock_api_client, monkeypatch, station):
|
|
_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_execute_page_shows_grab_button_with_camera(logged_in_client, mock_api_client, monkeypatch):
|
|
html = _page(logged_in_client, mock_api_client, monkeypatch, STATION)
|
|
assert "/measure/api/vision/grab" in html
|
|
assert "grab()" in html
|
|
|
|
|
|
def test_execute_page_hides_grab_button_without_camera(logged_in_client, mock_api_client, monkeypatch):
|
|
html = _page(logged_in_client, mock_api_client, monkeypatch, {**STATION, "camera_url": None})
|
|
assert "grab()" not in html
|
|
|
|
|
|
def test_execute_page_hides_grab_button_when_lookup_fails(logged_in_client, mock_api_client, monkeypatch):
|
|
html = _page(logged_in_client, mock_api_client, monkeypatch,
|
|
{"error": True, "status_code": 502, "detail": "down"})
|
|
assert "grab()" not in html
|