@@ -0,0 +1,173 @@
|
||||
"""Тесты Plymouth: рендер кадров и API админки."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app import create_app, db
|
||||
from app.routes.auth import _ensure_default_superuser
|
||||
from app.services import plymouth_frame_renderer as renderer
|
||||
from app.services import plymouth_theme_service as plymouth_svc
|
||||
from app.services.plymouth_theme_service import build_and_install_theme, get_plymouth_status
|
||||
from app.services.setup_state import mark_setup_complete
|
||||
from config import TestingConfig
|
||||
|
||||
|
||||
class PlymouthTestConfig(TestingConfig):
|
||||
_TMP = tempfile.mkdtemp(prefix="wesp-plymouth-test-")
|
||||
BASE_DIR = _TMP
|
||||
DATA_DIR = f"{_TMP}/data"
|
||||
SQLALCHEMY_DATABASE_URI = f"sqlite:///{_TMP}/recipes_test.db"
|
||||
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{_TMP}/reports_test.db"}
|
||||
AUTH_LOGIN = "admin"
|
||||
AUTH_PASSWORD = "admin-secret"
|
||||
SECRET_KEY = "unit-test-secret-key-not-default-32chars"
|
||||
|
||||
|
||||
class PlymouthThemeTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.app = create_app(PlymouthTestConfig)
|
||||
self.client = self.app.test_client()
|
||||
self.ctx = self.app.app_context()
|
||||
self.ctx.push()
|
||||
db.create_all()
|
||||
_ensure_default_superuser()
|
||||
mark_setup_complete(self.app)
|
||||
self._login()
|
||||
|
||||
def _login(self) -> None:
|
||||
r = self.client.post(
|
||||
"/api/auth/login",
|
||||
json={"login": "admin", "password": "admin-secret"},
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
self.ctx.pop()
|
||||
|
||||
def test_frame_count_matches_duration(self) -> None:
|
||||
self.assertGreater(renderer.frame_count(6000, 25), 100)
|
||||
self.assertEqual(renderer.animation_end_ms(6000), renderer.wave2_end_ms(6000) + 60)
|
||||
|
||||
def test_status_endpoint_requires_superuser(self) -> None:
|
||||
anon = create_app(PlymouthTestConfig)
|
||||
with anon.app_context():
|
||||
db.create_all()
|
||||
r = anon.test_client().get("/api/admin/plymouth/status")
|
||||
self.assertIn(r.status_code, (401, 403, 302))
|
||||
|
||||
def test_status_endpoint_ok(self) -> None:
|
||||
r = self.client.get("/api/admin/plymouth/status")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
data = r.get_json()
|
||||
self.assertEqual(data.get("status"), "success")
|
||||
self.assertEqual(data.get("resolution"), "800x600")
|
||||
self.assertIn("job", data)
|
||||
|
||||
@patch("app.services.plymouth_theme_service.build_and_install_theme")
|
||||
def test_install_starts_background_job(self, mock_build) -> None:
|
||||
r = self.client.post("/api/admin/plymouth/install")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.get_json().get("ok"))
|
||||
mock_build.assert_called_once()
|
||||
|
||||
@patch("app.services.plymouth_theme_service.build_and_install_theme")
|
||||
def test_install_busy_when_running(self, mock_build) -> None:
|
||||
from app.services import plymouth_theme_service as svc
|
||||
|
||||
with svc._lock:
|
||||
svc._job_state["running"] = True
|
||||
try:
|
||||
r = self.client.post("/api/admin/plymouth/install")
|
||||
self.assertEqual(r.status_code, 409)
|
||||
mock_build.assert_not_called()
|
||||
finally:
|
||||
with svc._lock:
|
||||
svc._job_state["running"] = False
|
||||
|
||||
def test_get_status_in_app(self) -> None:
|
||||
with self.app.app_context():
|
||||
st = get_plymouth_status(self.app)
|
||||
self.assertIn("plymouth_installed", st)
|
||||
self.assertIn("build_dir", st)
|
||||
|
||||
@patch("app.services.plymouth_theme_service._run_install_subprocess")
|
||||
@patch("app.services.plymouth_theme_service.render_all_frames")
|
||||
@patch("app.services.plymouth_theme_service.pillow_available", return_value=True)
|
||||
def test_build_thread_has_app_context(
|
||||
self, _pillow: object, mock_render: object, mock_install: object
|
||||
) -> None:
|
||||
from flask import current_app
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
logo = root / "static" / "logo2.png"
|
||||
if not logo.is_file():
|
||||
self.skipTest("logo2.png missing")
|
||||
|
||||
mock_render.return_value = {"frames": 2, "output_dir": "animation"}
|
||||
mock_install.return_value = {"ok": True, "message": "Тема установлена"}
|
||||
|
||||
errors: list[BaseException] = []
|
||||
|
||||
def worker() -> None:
|
||||
try:
|
||||
with self.app.test_request_context():
|
||||
build_and_install_theme(current_app)
|
||||
except BaseException as exc:
|
||||
errors.append(exc)
|
||||
|
||||
thread = threading.Thread(target=worker, name="test-plymouth")
|
||||
thread.start()
|
||||
thread.join(timeout=10)
|
||||
self.assertFalse(thread.is_alive(), "plymouth build thread hung")
|
||||
self.assertEqual(errors, [], errors[0] if errors else None)
|
||||
with plymouth_svc._lock:
|
||||
phase = plymouth_svc._job_state.get("phase")
|
||||
self.assertIn(phase, ("done", "error"))
|
||||
if phase == "error":
|
||||
self.assertNotIn(
|
||||
"application context",
|
||||
str(plymouth_svc._job_state.get("error", "")).lower(),
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(renderer.pillow_available(), "Pillow required")
|
||||
class PlymouthFrameRenderTests(unittest.TestCase):
|
||||
def test_render_single_frame(self) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
logo = root / "static" / "logo2.png"
|
||||
if not logo.is_file():
|
||||
self.skipTest("logo2.png missing")
|
||||
logo_img = renderer._load_logo_buffer(logo)
|
||||
cells = renderer._build_logo_cells(logo_img, renderer.INITIAL_CELL)
|
||||
layout = renderer._compute_logo_layout(800, 600, embedded=True)
|
||||
frame = renderer.render_frame(logo_img, cells, layout, 0)
|
||||
self.assertEqual(frame.size, (800, 600))
|
||||
|
||||
def test_render_all_frames_small(self) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
logo = root / "static" / "logo2.png"
|
||||
if not logo.is_file():
|
||||
self.skipTest("logo2.png missing")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp)
|
||||
summary = renderer.render_all_frames(
|
||||
logo,
|
||||
out,
|
||||
width=320,
|
||||
height=240,
|
||||
duration_ms=400,
|
||||
fps=10,
|
||||
)
|
||||
self.assertGreater(summary["frames"], 2)
|
||||
anim = out / "animation"
|
||||
self.assertTrue((anim / "0.png").is_file())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user