"""#118: una quota non misurata dalla visione e' una misura con value NULL (NaN), sempre fail. Il valore NULL non deve far cadere statistiche, report, CSV e liste, e non entra nei calcoli numerici (medie, indici, istogramma); conta solo nell'esito. """ import csv from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from src.backend.models.orm.measurement import Measurement from src.backend.models.orm.user import User from src.backend.services import measurement_service from src.backend.tests.conftest import auth_headers, create_test_recipe from src.backend.tests.test_statistics import _seed_measurements async def _recipe_with_nan(client, db_session, tec_user): """Una ricetta con tre misure numeriche e una NaN (value NULL, fail).""" recipe = await create_test_recipe(db_session, tec_user.id) subtask_id, version_id = await _seed_measurements( client, tec_user, recipe.id, [10.0, 10.1, 9.9], ) nan = await measurement_service.save_measurement( db_session, subtask_id=subtask_id, version_id=version_id, measured_by=tec_user.id, value=None, input_method="camera", enforce_tolerance_gate=False, ) await db_session.commit() return recipe, subtask_id, version_id, nan async def test_save_measurement_none_is_fail_without_deviation( client: AsyncClient, db_session: AsyncSession, measurement_tec_user: User, ): _, _, _, nan = await _recipe_with_nan(client, db_session, measurement_tec_user) assert nan.value is None assert nan.pass_fail == "fail" assert nan.deviation is None async def test_manual_create_still_requires_a_value( client: AsyncClient, db_session: AsyncSession, measurement_tec_user: User, ): """Solo il percorso visione puo' produrre NaN: la creazione manuale resta float.""" recipe = await create_test_recipe(db_session, measurement_tec_user.id) subtask_id, version_id = await _seed_measurements( client, measurement_tec_user, recipe.id, [], ) resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={"subtask_id": subtask_id, "version_id": version_id, "value": None}, ) assert resp.status_code == 422 async def test_statistics_skip_the_nan_measurement( client: AsyncClient, db_session: AsyncSession, measurement_tec_user: User, metrologist_user: User, ): recipe, subtask_id, _, _ = await _recipe_with_nan( client, db_session, measurement_tec_user, ) params = {"recipe_id": recipe.id, "subtask_id": subtask_id} headers = auth_headers(metrologist_user) cap = await client.get("/api/statistics/capability", headers=headers, params=params) assert cap.status_code == 200 assert cap.json()["n"] == 3 assert abs(cap.json()["mean"] - 10.0) < 1e-9 chart = await client.get("/api/statistics/control-chart", headers=headers, params=params) assert chart.status_code == 200 assert len(chart.json()["values"]) == 3 hist = await client.get("/api/statistics/histogram", headers=headers, params=params) assert hist.status_code == 200 summary = await client.get( "/api/statistics/summary", headers=headers, params={"recipe_id": recipe.id}, ) assert summary.status_code == 200 async def test_measurement_list_returns_null_value( client: AsyncClient, db_session: AsyncSession, measurement_tec_user: User, ): _, _, version_id, nan = await _recipe_with_nan(client, db_session, measurement_tec_user) resp = await client.get( "/api/measurements/", headers=auth_headers(measurement_tec_user), params={"version_id": version_id, "per_page": 50}, ) assert resp.status_code == 200 rows = {m["id"]: m for m in resp.json()["items"]} assert rows[nan.id]["value"] is None assert rows[nan.id]["pass_fail"] == "fail" assert rows[nan.id]["deviation"] is None async def test_csv_export_writes_nan( client: AsyncClient, db_session: AsyncSession, measurement_tec_user: User, ): _, _, _, nan = await _recipe_with_nan(client, db_session, measurement_tec_user) resp = await client.get( "/api/measurements/export/csv", headers=auth_headers(measurement_tec_user), ) assert resp.status_code == 200 rows = list(csv.reader(resp.text.splitlines())) header = rows[0] by_id = {r[header.index("id")]: r for r in rows[1:]} row = by_id[str(nan.id)] assert row[header.index("value")] == "NaN" assert row[header.index("pass_fail")] == "fail" assert row[header.index("deviation")] == "NaN" # #124