Compare commits

..

6 Commits

Author SHA1 Message Date
Adriano d652891c1c Merge lavoro/daitarn-tmf-81 (TieMeasureFlow#81)
rilascio-windows / setup-windows (push) Successful in 34m11s
2026-10-17 14:25:56 +02:00
Adriano 0b22643d69 Merge lavoro/daitarn-tmf-80 (TieMeasureFlow#80) 2026-10-17 14:25:56 +02:00
Adriano 15e31e2b8a Merge lavoro/daitarn-tmf-79 (TieMeasureFlow#79) 2026-10-17 14:25:55 +02:00
Adriano f8f5f248ff setup: init-db e reset-db stampano head in un thread (TieMeasureFlow#80)
_stamp_head esegue env.py di alembic, che fa asyncio.run() e dentro il loop
di FastAPI dava RuntimeError (500). Ora gira con asyncio.to_thread. Nuovo
test_setup_stamp.py senza monkeypatch di _stamp_head, su file SQLite.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 14:25:24 +02:00
Adriano f33415df6a Test frontend: niente piu' dipendenza dall'ordine sulla stazione (TieMeasureFlow#79)
test_measure.py non ricarica piu' config/blueprints.measure (monkeypatch.setattr,
annullato al teardown); TestTaskComplete e test_recipe_rules_ui.py fissano la
stazione esplicitamente con una fixture autouse.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 14:13:15 +02:00
Adriano 62b5e412ca static del frontend sempre LF (.gitattributes): impronte di VERSIONS.md e pacchetto Windows identici (TieMeasureFlow#81)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:10:39 +02:00
5 changed files with 121 additions and 27 deletions
+3
View File
@@ -0,0 +1,3 @@
# TieMeasureFlow#81: static del frontend sempre con LF, anche con core.autocrlf=true su Windows:
# le impronte sha256 di static/vendor/VERSIONS.md (test_offline.py) e il pacchetto Windows le vogliono identiche.
src/frontend/flask_app/static/** text=auto eol=lf
+7 -2
View File
@@ -3,6 +3,7 @@
Protected by SETUP_PASSWORD env var. When empty/None, all endpoints return 404. Protected by SETUP_PASSWORD env var. When empty/None, all endpoints return 404.
NOT protected by API Key auth (standalone access). NOT protected by API Key auth (standalone access).
""" """
import asyncio
import hmac import hmac
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -312,7 +313,9 @@ async def init_db(body: PasswordBody):
await _require_not_initialised() await _require_not_initialised()
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
_stamp_head() # #80: env.py di alembic fa asyncio.run(), vietato dentro il loop di FastAPI;
# in un thread non c'e' un loop attivo e funziona.
await asyncio.to_thread(_stamp_head)
return {"status": "ok", "message": "Database tables created successfully"} return {"status": "ok", "message": "Database tables created successfully"}
@@ -748,6 +751,8 @@ async def reset_db(body: ResetDbBody):
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
_stamp_head() # #80: env.py di alembic fa asyncio.run(), vietato dentro il loop di FastAPI;
# in un thread non c'e' un loop attivo e funziona.
await asyncio.to_thread(_stamp_head)
return {"status": "ok", "message": "Database reset successfully"} return {"status": "ok", "message": "Database reset successfully"}
+81
View File
@@ -0,0 +1,81 @@
"""init-db / reset-db con _stamp_head REALE su un file SQLite (TieMeasureFlow#80).
_stamp_head esegue migrations/env.py, che chiama asyncio.run(): dentro il loop
gia' attivo di FastAPI dava RuntimeError -> 500. test_setup.py e
test_station_seed.py sostituiscono _stamp_head con un no-op e non lo vedevano:
qui NON si fa monkeypatch di _stamp_head.
"""
import logging.config
import sqlite3
import pytest
from alembic.config import Config
from alembic.script import ScriptDirectory
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from src.backend.config import settings
from src.backend.main import app
SETUP_PWD = "test-setup-password"
@pytest.fixture
def file_db(tmp_path, monkeypatch):
"""Punta settings, engine e session factory del router setup a un file SQLite."""
import src.backend.api.routers.setup as setup_mod
db_file = tmp_path / "t.db"
url = f"sqlite+aiosqlite:///{db_file.as_posix()}"
# env.py ricava sqlalchemy.url da settings.database_url (property su db_url)
monkeypatch.setattr(settings, "db_url", url)
monkeypatch.setattr(settings, "setup_password", SETUP_PWD)
file_engine = create_async_engine(url, echo=False)
monkeypatch.setattr(setup_mod, "engine", file_engine)
monkeypatch.setattr(
setup_mod, "async_session_factory",
async_sessionmaker(file_engine, expire_on_commit=False),
)
# env.py chiama fileConfig, che disattiverebbe i logger degli altri test
monkeypatch.setattr(logging.config, "fileConfig", lambda *a, **k: None)
return db_file, setup_mod, file_engine
def _head() -> str:
from src.backend.api.routers.setup import _alembic_ini
cfg = Config(str(_alembic_ini))
return ScriptDirectory.from_config(cfg).get_current_head()
def _versions(db_file) -> list[str]:
con = sqlite3.connect(db_file)
try:
return [r[0] for r in con.execute("SELECT version_num FROM alembic_version")]
finally:
con.close()
@pytest.fixture
async def http():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
yield ac
@pytest.mark.asyncio
async def test_init_db_stamps_head(file_db, http):
db_file, _, file_engine = file_db
resp = await http.post("/api/setup/init-db", json={"password": SETUP_PWD})
assert resp.status_code == 200, resp.text
assert _versions(db_file) == [_head()]
await file_engine.dispose()
@pytest.mark.asyncio
async def test_reset_db_stamps_head(file_db, http):
db_file, _, file_engine = file_db
resp = await http.post("/api/setup/init-db", json={"password": SETUP_PWD})
assert resp.status_code == 200, resp.text
resp = await http.post("/api/setup/reset-db", json={"password": SETUP_PWD, "confirm": True})
assert resp.status_code == 200, resp.text
assert _versions(db_file) == [_head()]
await file_engine.dispose()
+16 -18
View File
@@ -12,12 +12,9 @@ class TestSelectRecipe:
def test_select_recipe_renders(self, logged_in_client, mock_api_client, monkeypatch): def test_select_recipe_renders(self, logged_in_client, mock_api_client, monkeypatch):
"""Recipe selection page renders for MeasurementTec role.""" """Recipe selection page renders for MeasurementTec role."""
monkeypatch.setenv("STATION_CODE", "ST-TEST") # Patch the class attribute instead of reload()ing config/blueprints.measure:
import config # monkeypatch undoes it at teardown, so no ST-TEST leaks into later tests (#79).
import importlib monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
mock_api_client.get_station_recipes.return_value = [ mock_api_client.get_station_recipes.return_value = [
{"id": 1, "code": "REC-001", "name": "Test Recipe"}, {"id": 1, "code": "REC-001", "name": "Test Recipe"},
@@ -47,12 +44,9 @@ class TestSelectRecipe:
import re import re
from html.parser import HTMLParser from html.parser import HTMLParser
monkeypatch.setenv("STATION_CODE", "ST-TEST") # Patch the class attribute instead of reload()ing config/blueprints.measure:
import config # monkeypatch undoes it at teardown, so no ST-TEST leaks into later tests (#79).
import importlib monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
mock_api_client.get_station_recipes.return_value = [] mock_api_client.get_station_recipes.return_value = []
@@ -79,12 +73,9 @@ class TestSelectRecipe:
self, logged_in_client, mock_api_client, monkeypatch self, logged_in_client, mock_api_client, monkeypatch
): ):
"""#10: a lot number like ``L'23`` must not break the page.""" """#10: a lot number like ``L'23`` must not break the page."""
monkeypatch.setenv("STATION_CODE", "ST-TEST") # Patch the class attribute instead of reload()ing config/blueprints.measure:
import config # monkeypatch undoes it at teardown, so no ST-TEST leaks into later tests (#79).
import importlib monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
mock_api_client.get_station_recipes.return_value = [] mock_api_client.get_station_recipes.return_value = []
@@ -393,6 +384,13 @@ class TestStartSequence:
class TestTaskComplete: class TestTaskComplete:
"""GET /measure/complete/<recipe_id> tests.""" """GET /measure/complete/<recipe_id> tests."""
@pytest.fixture(autouse=True)
def _station(self, monkeypatch):
# The side_effect lists below include the /production-runs/current reply,
# which is only requested when a station is configured: pin it explicitly
# instead of relying on whatever an earlier test left behind (#79).
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
def test_task_complete_renders(self, logged_in_client, mock_api_client): def test_task_complete_renders(self, logged_in_client, mock_api_client):
"""The summary carries the measurement clock, so it has to render with it.""" """The summary carries the measurement clock, so it has to render with it."""
mock_api_client.get.side_effect = [ mock_api_client.get.side_effect = [
@@ -6,6 +6,17 @@ a start button that will not start, a description that reads the way it was
written. written.
""" """
import pytest
@pytest.fixture(autouse=True)
def _no_station(monkeypatch):
# These tests assume no station is configured (e.g. _task_list feeds the mock a
# fixed number of replies, with no /production-runs/current among them); pin it
# so a STATION_CODE left behind by an earlier test cannot change the call count (#79).
# Tests that need a station set it themselves, after this fixture has run.
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", None)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Point 11 - line breaks and bold, and nothing else # Point 11 - line breaks and bold, and nothing else
@@ -197,13 +208,9 @@ def test_avvia_is_untouched_when_nothing_is_required(
def test_selection_screen_carries_the_traceability_rules( def test_selection_screen_carries_the_traceability_rules(
logged_in_client, mock_api_client, monkeypatch, logged_in_client, mock_api_client, monkeypatch,
): ):
monkeypatch.setenv("STATION_CODE", "ST-TEST") # Patch instead of reload()ing config/blueprints.measure: the patch is undone at
import importlib # teardown, a reload would leave ST-TEST behind for later tests (#79).
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
import config
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
mock_api_client.get_station_recipes.return_value = [ mock_api_client.get_station_recipes.return_value = [
{ {