Files
Presa-Mano/tests/test_right_arm_routes.py
T

229 lines
9.0 KiB
Python

"""Integration test del router /api/right_arm/*."""
from __future__ import annotations
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from presa_mano.main import app
client = TestClient(app)
def _fake_wrist_response():
return {
"left_wrist": {"position": [0.10, 0.20, 0.30], "quaternion": [0, 0, 0, 1]},
"right_wrist": {"position": [0.30, -0.20, 0.30], "quaternion": [0, 0, 0, 1]},
}
def test_state_returns_wrist_pose_and_config():
with patch("presa_mano.api.right_arm_routes._deps.g1") as g1_factory:
g1 = MagicMock()
g1.wrist.return_value = _fake_wrist_response()
g1_factory.return_value = g1
r = client.get("/api/right_arm/state")
assert r.status_code == 200
body = r.json()
assert body["wrist"]["position"] == [0.30, -0.20, 0.30]
assert "rpy_deg" in body["wrist"]
assert body["side"] == "right" or body["side"] in ("left", "right")
assert "home_pose" in body
assert "custom_pose" in body
assert "offset_camera_mm" in body
assert "axis_signs" in body
assert "max_step_m" in body
assert "z_floor_m" in body
assert "robot_active" in body
assert "timings_ms" in body
def test_state_502_when_g1_unavailable():
from presa_mano.clients import G1Unavailable
with patch("presa_mano.api.right_arm_routes._deps.g1") as g1_factory:
g1 = MagicMock()
g1.wrist.side_effect = G1Unavailable("connection refused")
g1_factory.return_value = g1
r = client.get("/api/right_arm/state")
assert r.status_code == 502
def test_activate_sets_robot_active_g1_and_side_right():
# /activate è idempotente: chiama PUT /api/config e ritorna /state
with patch("presa_mano.api.right_arm_routes._deps.g1") as g1_factory:
g1 = MagicMock()
g1.wrist.return_value = _fake_wrist_response()
g1_factory.return_value = g1
r = client.post("/api/right_arm/activate")
assert r.status_code == 200
body = r.json()
assert body["robot_active"] == "g1"
assert body["side"] == "right"
def test_snapshot_front_returns_jpeg():
fake_jpeg = b"\xff\xd8\xff" + b"\x00" * 100 # magic JPEG bytes
with patch("presa_mano.api.right_arm_routes._deps.front_camera") as factory:
cam = MagicMock()
cam.rgb_jpeg.return_value = fake_jpeg
factory.return_value = cam
r = client.get("/api/right_arm/snapshot/front")
assert r.status_code == 200
assert r.headers["content-type"].startswith("image/jpeg")
assert r.content == fake_jpeg
def test_snapshot_wrist_returns_jpeg():
fake_jpeg = b"\xff\xd8\xff" + b"\x00" * 50
with patch("presa_mano.api.right_arm_routes._deps.nuwa") as factory:
cam = MagicMock()
cam.rgb_jpeg.return_value = fake_jpeg
factory.return_value = cam
r = client.get("/api/right_arm/snapshot/wrist")
assert r.status_code == 200
assert r.headers["content-type"].startswith("image/jpeg")
assert r.content == fake_jpeg
def test_snapshot_front_502_on_failure():
with patch("presa_mano.api.right_arm_routes._deps.front_camera") as factory:
cam = MagicMock()
cam.rgb_jpeg.side_effect = RuntimeError("connection refused")
factory.return_value = cam
r = client.get("/api/right_arm/snapshot/front")
assert r.status_code == 502
def _fake_yolo_top(class_name="pestello", conf=0.87):
return {"detections": [
{"class": class_name, "confidence": conf,
"bbox": {"x1": 100.0, "y1": 80.0, "x2": 220.0, "y2": 180.0}},
{"class": "altra", "confidence": 0.5,
"bbox": {"x1": 10.0, "y1": 10.0, "x2": 50.0, "y2": 50.0}},
]}
def test_detect_front_filters_class_and_returns_top():
with patch("presa_mano.api.right_arm_routes._deps.yolo") as ying, \
patch("presa_mano.api.right_arm_routes._deps.front_camera") as cam_f:
yolo = MagicMock()
yolo.detect_url.return_value = _fake_yolo_top()
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://192.168.123.99:8878/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
json={"class_name": "pestello", "conf": 0.4})
assert r.status_code == 200
body = r.json()
assert body["detections"][0]["class"] == "pestello"
assert body["image_size"] == [640, 480]
def test_detect_front_404_when_no_match():
with patch("presa_mano.api.right_arm_routes._deps.yolo") as ying, \
patch("presa_mano.api.right_arm_routes._deps.front_camera") as cam_f:
yolo = MagicMock()
yolo.detect_url.return_value = {"detections": []}
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://x/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
json={"class_name": "pestello"})
assert r.status_code == 404
def test_detect_front_falls_back_to_proxy_url_on_yolo_failure():
"""Se YOLO non riesce sull'URL diretto, ripeti con `proxy_base_url`."""
attempts = []
def _detect_url(image_url, conf=None):
attempts.append(image_url)
if "192.168" in image_url:
raise RuntimeError("dns timeout")
return _fake_yolo_top()
with patch("presa_mano.api.right_arm_routes._deps.yolo") as ying, \
patch("presa_mano.api.right_arm_routes._deps.front_camera") as cam_f:
yolo = MagicMock()
yolo.detect_url.side_effect = _detect_url
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://192.168.123.99:8878/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
json={"class_name": "pestello",
"proxy_base_url": "http://localhost:8890"})
assert r.status_code == 200
assert len(attempts) == 2
assert "192.168" in attempts[0]
assert attempts[1].endswith("/api/right_arm/snapshot/front")
def test_detect_wrist_returns_centroid_and_distance():
# YOLO → bbox → SAM21 → centroide
import numpy as np
fake_points = np.array([
[0.01, -0.02, 0.20],
[0.02, -0.01, 0.21],
[0.00, -0.03, 0.19],
] * 5, dtype=np.float64) # 15 punti, > 10 minimo
with patch("presa_mano.api.right_arm_routes._deps.nuwa") as nuwa_f, \
patch("presa_mano.api.right_arm_routes._deps.yolo") as yolo_f, \
patch("presa_mano.api.right_arm_routes.segment_with_sam21") as seg_f, \
patch("presa_mano.api.right_arm_routes._deps.sam21") as sam_f:
nuwa = MagicMock()
nuwa.rgb_url.return_value = "http://localhost:8877/api/rgb"
nuwa_f.return_value = nuwa
yolo = MagicMock()
yolo.detect_url.return_value = _fake_yolo_top()
yolo_f.return_value = yolo
sam_f.return_value = MagicMock()
seg = MagicMock()
seg.points_cv_m = fake_points
seg_f.return_value = seg
r = client.post("/api/right_arm/detect/wrist",
json={"class_name": "pestello"})
assert r.status_code == 200
body = r.json()
assert body["detection"]["class"] == "pestello"
assert "bbox" in body
c = body["centroid_camera_m"]
assert abs(c[2] - 0.20) < 0.05
assert body["distance_m"] > 0
assert body["n_sam21_points"] == 15
def test_detect_wrist_422_too_few_points():
import numpy as np
with patch("presa_mano.api.right_arm_routes._deps.nuwa") as nuwa_f, \
patch("presa_mano.api.right_arm_routes._deps.yolo") as yolo_f, \
patch("presa_mano.api.right_arm_routes.segment_with_sam21") as seg_f, \
patch("presa_mano.api.right_arm_routes._deps.sam21") as sam_f:
nuwa = MagicMock(); nuwa.rgb_url.return_value = "http://x"; nuwa_f.return_value = nuwa
yolo = MagicMock(); yolo.detect_url.return_value = _fake_yolo_top(); yolo_f.return_value = yolo
sam_f.return_value = MagicMock()
seg = MagicMock(); seg.points_cv_m = np.zeros((3, 3))
seg_f.return_value = seg
r = client.post("/api/right_arm/detect/wrist",
json={"class_name": "pestello"})
assert r.status_code == 422
def test_detect_wrist_404_when_yolo_no_match():
with patch("presa_mano.api.right_arm_routes._deps.nuwa") as nuwa_f, \
patch("presa_mano.api.right_arm_routes._deps.yolo") as yolo_f:
nuwa = MagicMock(); nuwa.rgb_url.return_value = "http://x"; nuwa_f.return_value = nuwa
yolo = MagicMock(); yolo.detect_url.return_value = {"detections": []}; yolo_f.return_value = yolo
r = client.post("/api/right_arm/detect/wrist", json={"class_name": "pestello"})
assert r.status_code == 404