Compare commits

...

5 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 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
3 changed files with 91 additions and 2 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()