Actualizar a 1.1.4 fallaba con "no escribio config\.version tras la actualizacion" aunque el binario nuevo estuviera instalado y corriendo desde la ruta correcta. La causa era el ORDEN dentro de ensure_runtime_layout(): el sello iba al final, detras del re-despliegue de las deps embebidas (7-Zip y ODBC). Al cambiar de version esas deps se re-copian ENTERAS, asi que el sello quedaba por detras de esa copia y del desempaquetado del onefile de ~254 MB con el antivirus escaneando cada archivo. El PANEL se rendia esperandolo y daba por fallida una actualizacion que iba bien. - El sello se escribe lo primero, en cuanto existen las carpetas. Es tambien mas honesto sobre lo que significa —"que binario esta corriendo"—, que es cierto desde que el proceso arranca. El sello de DEPS sigue yendo al final, donde su comentario explica por que: si la copia falla a medias, el proximo arranque reintenta en vez de quedar marcado como al dia. - La version va en la PRIMERA linea del log de arranque. Permite comprobar que binario corre de verdad mirando solo config/logs, sin depender del sello ni del reporte al panel: verificar una actualizacion deja de obligar a creerse lo que diga otro sistema. La prueba nueva observa el estado del sello EN EL MOMENTO en que empieza la copia de deps, no al final, que es la unica forma de fijar el orden. Comprobado que muerde: devolviendo el sello al final falla con `assert None == '1.1.5'`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
224 lines
9.5 KiB
Python
224 lines
9.5 KiB
Python
"""Pruebas de bootstrap y carga de .env."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.config import bootstrap
|
|
from app.config.bootstrap import ensure_runtime_layout
|
|
from app.config.env_loader import (
|
|
apply_env_overrides,
|
|
get_launch_options,
|
|
is_panel_configured,
|
|
render_env_template,
|
|
)
|
|
from app.constants import CONFIG_DIR, DEFAULT_CONFIG, DIR_ENTRADA, ENV_PATH
|
|
|
|
|
|
def test_render_env_template_contains_app_dir(tmp_path: Path):
|
|
text = render_env_template(tmp_path)
|
|
assert str(tmp_path) in text
|
|
assert "CLOUDRESTORE_PANEL_API_URL" in text
|
|
|
|
|
|
def test_ensure_runtime_layout_creates_structure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr("app.config.bootstrap.APP_DIR", tmp_path)
|
|
monkeypatch.setattr("app.config.bootstrap.CONFIG_DIR", tmp_path / "config")
|
|
monkeypatch.setattr("app.config.bootstrap.DATA_DIR", tmp_path / "config" / "data")
|
|
monkeypatch.setattr("app.config.bootstrap.LOGS_DIR", tmp_path / "config" / "logs")
|
|
monkeypatch.setattr("app.config.bootstrap.ODBC_DIR", tmp_path / "config" / "odbc")
|
|
monkeypatch.setattr("app.config.bootstrap.SEVEN_ZIP_DIR", tmp_path / "config" / "7zip")
|
|
monkeypatch.setattr("app.config.bootstrap.ENV_PATH", tmp_path / "config" / ".env")
|
|
monkeypatch.setattr("app.config.bootstrap.DIR_ENTRADA", tmp_path / "Entrada")
|
|
monkeypatch.setattr("app.config.bootstrap.DIR_PROCESADOS", tmp_path / "Procesados")
|
|
monkeypatch.setattr("app.config.bootstrap.DIR_FALLADOS", tmp_path / "Fallados")
|
|
monkeypatch.setattr("app.config.bootstrap.DIR_TEMP", tmp_path / "Temp")
|
|
monkeypatch.setattr("app.constants.DB_PATH", tmp_path / "config" / "data" / "app.db")
|
|
|
|
ensure_runtime_layout()
|
|
|
|
assert (tmp_path / "config" / ".env").is_file()
|
|
assert (tmp_path / "Entrada").is_dir()
|
|
assert (tmp_path / "config" / "data" / "app.db").is_file()
|
|
|
|
|
|
def test_is_panel_configured(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_URL", raising=False)
|
|
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_TOKEN", raising=False)
|
|
monkeypatch.delenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", raising=False)
|
|
assert not is_panel_configured()
|
|
|
|
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_URL", "https://panel:3000")
|
|
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_TOKEN", "secret")
|
|
monkeypatch.setenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "srv1")
|
|
assert is_panel_configured()
|
|
|
|
|
|
def test_get_launch_options_auto_start_does_not_minimize(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("CLOUDRESTORE_AUTO_START", "true")
|
|
monkeypatch.delenv("CLOUDRESTORE_START_MINIMIZED", raising=False)
|
|
opts = get_launch_options()
|
|
assert opts.start_engine is True
|
|
assert opts.minimized is False
|
|
|
|
|
|
def test_get_launch_options_start_minimized(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("CLOUDRESTORE_START_MINIMIZED", "true")
|
|
opts = get_launch_options()
|
|
assert opts.minimized is True
|
|
|
|
|
|
def test_render_env_template_documents_start_minimized(tmp_path: Path):
|
|
text = render_env_template(tmp_path)
|
|
assert "CLOUDRESTORE_START_MINIMIZED=false" in text
|
|
|
|
|
|
def test_apply_env_overrides_paths(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("CLOUDRESTORE_INPUT_FOLDER", "/tmp/in")
|
|
cfg = apply_env_overrides(DEFAULT_CONFIG.copy())
|
|
assert cfg["paths"]["input_folder"] == "/tmp/in"
|
|
|
|
|
|
def _redirect_bootstrap(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Reapunta los globales de bootstrap a un árbol temporal. Devuelve config/."""
|
|
for name, rel in (
|
|
("APP_DIR", ""),
|
|
("CONFIG_DIR", "config"),
|
|
("DATA_DIR", "config/data"),
|
|
("LOGS_DIR", "config/logs"),
|
|
("ODBC_DIR", "config/odbc"),
|
|
("SEVEN_ZIP_DIR", "config/7zip"),
|
|
("ENV_PATH", "config/.env"),
|
|
("DIR_ENTRADA", "Entrada"),
|
|
("DIR_PROCESADOS", "Procesados"),
|
|
("DIR_FALLADOS", "Fallados"),
|
|
("DIR_TEMP", "Temp"),
|
|
):
|
|
target = tmp_path / rel if rel else tmp_path
|
|
monkeypatch.setattr(f"app.config.bootstrap.{name}", target)
|
|
monkeypatch.setattr("app.constants.DB_PATH", tmp_path / "config" / "data" / "app.db")
|
|
return tmp_path / "config"
|
|
|
|
|
|
def test_bootstrap_escribe_sello_de_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""
|
|
El instalador remoto del PANEL lee config/.version por SFTP para verificar el
|
|
despliegue: en Windows el .exe se compila con console=False y no hay stdout confiable.
|
|
"""
|
|
from app import __version__
|
|
|
|
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
|
|
ensure_runtime_layout()
|
|
assert (config_dir / ".version").read_text(encoding="utf-8").strip() == __version__
|
|
|
|
|
|
def test_sello_de_version_se_escribe_antes_de_copiar_las_deps(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""
|
|
El sello tiene que existir ANTES de re-desplegar 7-Zip y ODBC.
|
|
|
|
Iba al final, y eso lo volvía inservible justo cuando más importa: al cambiar de versión las
|
|
deps embebidas se re-copian enteras, así que el sello quedaba por detrás de esa copia y del
|
|
desempaquetado del onefile de ~254 MB con el antivirus escaneando. El PANEL se rendía
|
|
esperándolo y reportaba como fallida una actualización que en realidad iba bien.
|
|
"""
|
|
from app import __version__
|
|
|
|
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
|
|
|
|
# Se observa el estado del sello EN EL MOMENTO en que empieza la copia de deps.
|
|
visto: dict[str, str | None] = {}
|
|
real = bootstrap._copy_bundled_tree
|
|
|
|
def espia(*args, **kwargs):
|
|
sello = config_dir / ".version"
|
|
visto.setdefault(
|
|
"al_copiar",
|
|
sello.read_text(encoding="utf-8").strip() if sello.exists() else None,
|
|
)
|
|
return real(*args, **kwargs)
|
|
|
|
monkeypatch.setattr("app.config.bootstrap._copy_bundled_tree", espia)
|
|
ensure_runtime_layout()
|
|
|
|
assert visto["al_copiar"] == __version__, (
|
|
"el sello de versión debe existir antes de empezar a copiar las deps embebidas"
|
|
)
|
|
|
|
|
|
def test_bootstrap_redespliega_deps_cuando_cambia_el_manifiesto(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""
|
|
config/7zip y config/odbc se re-copian cuando el build trae otras versiones embebidas.
|
|
Antes solo se copiaban si la carpeta estaba vacía, así que una actualización con driver
|
|
ODBC nuevo conservaba el viejo indefinidamente.
|
|
"""
|
|
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
|
|
|
|
src_7zip = tmp_path / "bundle" / "7zip"
|
|
src_odbc = tmp_path / "bundle" / "odbc"
|
|
src_7zip.mkdir(parents=True)
|
|
src_odbc.mkdir(parents=True)
|
|
(src_7zip / "7zz").write_text("7zz v26", encoding="utf-8")
|
|
(src_odbc / "odbcinst.ini").write_text("odbc 18.5", encoding="utf-8")
|
|
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_7ZIP", src_7zip)
|
|
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_ODBC", src_odbc)
|
|
|
|
# bundled-versions.json se busca bajo BUNDLE_DIR/packaging (va embebido en el onefile).
|
|
bundle_root = tmp_path / "bundle_root"
|
|
(bundle_root / "packaging").mkdir(parents=True)
|
|
manifest = bundle_root / "packaging" / "bundled-versions.json"
|
|
manifest.write_text('{"seven_zip": "26.01"}', encoding="utf-8")
|
|
monkeypatch.setattr("app.config.bootstrap.BUNDLE_DIR", bundle_root)
|
|
|
|
ensure_runtime_layout()
|
|
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "7zz v26"
|
|
stamp_before = (config_dir / ".bundled_deps").read_text(encoding="utf-8").strip()
|
|
assert stamp_before.startswith("sha256:")
|
|
|
|
# Mismo manifiesto: no debe re-copiar (no pisa ajustes locales en cada arranque).
|
|
(config_dir / "7zip" / "7zz").write_text("editado a mano", encoding="utf-8")
|
|
ensure_runtime_layout()
|
|
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "editado a mano"
|
|
|
|
# Manifiesto distinto (build con deps nuevas): debe re-copiar.
|
|
(src_7zip / "7zz").write_text("7zz v27", encoding="utf-8")
|
|
(src_odbc / "odbcinst.ini").write_text("odbc 19.0", encoding="utf-8")
|
|
manifest.write_text('{"seven_zip": "27.00"}', encoding="utf-8")
|
|
ensure_runtime_layout()
|
|
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "7zz v27"
|
|
assert (config_dir / "odbc" / "odbcinst.ini").read_text(encoding="utf-8") == "odbc 19.0"
|
|
assert (config_dir / ".bundled_deps").read_text(encoding="utf-8").strip() != stamp_before
|
|
|
|
# El re-despliegue SOBRESCRIBE, así que lo que el operador hubiera editado a mano debe
|
|
# quedar respaldado: no es reconstruible.
|
|
assert (config_dir / "7zip.bak" / "7zz").read_text(encoding="utf-8") == "editado a mano"
|
|
assert (config_dir / "odbc.bak" / "odbcinst.ini").read_text(encoding="utf-8") == "odbc 18.5"
|
|
|
|
|
|
def test_bootstrap_no_respalda_cuando_no_hay_refresco(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Sin cambio de manifiesto no se re-copia, así que tampoco debe crearse el .bak."""
|
|
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
|
|
|
|
src_7zip = tmp_path / "bundle" / "7zip"
|
|
src_7zip.mkdir(parents=True)
|
|
(src_7zip / "7zz").write_text("7zz v26", encoding="utf-8")
|
|
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_7ZIP", src_7zip)
|
|
|
|
bundle_root = tmp_path / "bundle_root"
|
|
(bundle_root / "packaging").mkdir(parents=True)
|
|
(bundle_root / "packaging" / "bundled-versions.json").write_text(
|
|
'{"seven_zip": "26.01"}', encoding="utf-8"
|
|
)
|
|
monkeypatch.setattr("app.config.bootstrap.BUNDLE_DIR", bundle_root)
|
|
|
|
ensure_runtime_layout()
|
|
ensure_runtime_layout() # segunda vez: mismo manifiesto, no debe refrescar
|
|
|
|
assert not (config_dir / "7zip.bak").exists()
|