263 lines
10 KiB
Python
263 lines
10 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""E2E: Generatoren, Remote-Server-Medienverteilung, Autostart-Merge.
|
||
|
|
|
||
|
|
Startet ZWEI echte Server: A (lokal) und B (Quelle). Server A
|
||
|
|
registriert B und zieht Medien von B ins lokale Archiv.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
import urllib.error
|
||
|
|
import urllib.parse
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
ROOT = "/a0/usr/workdir/hms-mediaengine"
|
||
|
|
DATA_B = "/tmp/hms_server_b_data"
|
||
|
|
PORT_A = 8095
|
||
|
|
PORT_B = 8096
|
||
|
|
BASE_A = "http://localhost:%d" % PORT_A
|
||
|
|
BASE_B = "http://localhost:%d" % PORT_B
|
||
|
|
|
||
|
|
procs = {}
|
||
|
|
|
||
|
|
|
||
|
|
def start_server(tag: str, base: str, port: int,
|
||
|
|
data_root: str | None = None) -> bool:
|
||
|
|
cmd = ["/usr/bin/python3", ROOT + "/run.py",
|
||
|
|
"--port", str(port)]
|
||
|
|
if data_root:
|
||
|
|
cmd += ["--data-root", data_root]
|
||
|
|
procs[tag] = subprocess.Popen(
|
||
|
|
cmd,
|
||
|
|
cwd=ROOT, stdout=open("/tmp/hms_e2e_%s.log" % tag, "ab"),
|
||
|
|
stderr=subprocess.STDOUT)
|
||
|
|
deadline = time.time() + 25
|
||
|
|
while time.time() < deadline:
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(base + "/api/health",
|
||
|
|
timeout=2) as r:
|
||
|
|
if (json.loads(r.read().decode())
|
||
|
|
.get("status") == "ok"):
|
||
|
|
return True
|
||
|
|
except Exception: # noqa: BLE001
|
||
|
|
time.sleep(0.3)
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def stop_all():
|
||
|
|
for p in procs.values():
|
||
|
|
p.terminate()
|
||
|
|
for p in procs.values():
|
||
|
|
try:
|
||
|
|
p.wait(timeout=6)
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
p.kill()
|
||
|
|
procs.clear()
|
||
|
|
time.sleep(0.5)
|
||
|
|
|
||
|
|
|
||
|
|
def get(base, path):
|
||
|
|
with urllib.request.urlopen(base + path, timeout=15) as r:
|
||
|
|
return json.loads(r.read().decode())
|
||
|
|
|
||
|
|
|
||
|
|
def post(base, path, body=None, raw=None, headers=None):
|
||
|
|
data = raw if raw is not None else json.dumps(body or {}).encode()
|
||
|
|
req = urllib.request.Request(base + path, data=data, method="POST")
|
||
|
|
req.add_header("Content-Type", "application/json" if raw is None
|
||
|
|
else "application/octet-stream")
|
||
|
|
for k, v in (headers or {}).items():
|
||
|
|
req.add_header(k, v)
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(req, timeout=180) as r:
|
||
|
|
return json.loads(r.read().decode())
|
||
|
|
except urllib.error.HTTPError as e:
|
||
|
|
return json.loads(e.read().decode())
|
||
|
|
|
||
|
|
|
||
|
|
def check(label, ok, detail=""):
|
||
|
|
print(("PASS" if ok else "FAIL") + " " + label + " " + str(detail))
|
||
|
|
if not ok:
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------- Reset ----------
|
||
|
|
for d in ("media", "thumbs", "projects"):
|
||
|
|
shutil.rmtree(os.path.join(ROOT, d), ignore_errors=True)
|
||
|
|
try:
|
||
|
|
os.remove(os.path.join(ROOT, "config.json"))
|
||
|
|
except FileNotFoundError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
try:
|
||
|
|
# ---------- Server B (Quelle) ----------
|
||
|
|
shutil.rmtree(DATA_B, ignore_errors=True)
|
||
|
|
check("Server B startet (eigener Datenordner)",
|
||
|
|
start_server("B", BASE_B, PORT_B, data_root=DATA_B))
|
||
|
|
|
||
|
|
# Testvideo auf B erzeugen + hochladen
|
||
|
|
subprocess.run([
|
||
|
|
"/usr/bin/python3", "-c",
|
||
|
|
"import gi; gi.require_version('Gst','1.0')\n"
|
||
|
|
"from gi.repository import Gst; Gst.init(None)\n"
|
||
|
|
"p = Gst.parse_launch('videotestsrc num-buffers=60 pattern=18 ! "
|
||
|
|
"video/x-raw,width=320,height=180 ! x264enc tune=zerolatency ! "
|
||
|
|
"mp4mux ! filesink location=/tmp/e2e_dist.mp4')\n"
|
||
|
|
"p.set_state(Gst.State.PLAYING)\n"
|
||
|
|
"p.get_bus().timed_pop_filtered(Gst.CLOCK_TIME_NONE, "
|
||
|
|
"Gst.MessageType.EOS)\n"
|
||
|
|
"p.set_state(Gst.State.NULL)\n",
|
||
|
|
], check=True)
|
||
|
|
data = open("/tmp/e2e_dist.mp4", "rb").read()
|
||
|
|
r = post(BASE_B, "/api/media/upload", raw=data,
|
||
|
|
headers={"X-Filename": urllib.parse.quote("clip_vom_server_B.mp4")})
|
||
|
|
check("B: Video hochgeladen", bool(r.get("saved")), r)
|
||
|
|
|
||
|
|
# ---------- Server A (Hauptserver) ----------
|
||
|
|
check("Server A startet", start_server("A", BASE_A, PORT_A))
|
||
|
|
|
||
|
|
# 1) Generatoren: Katalog + Layer + FX
|
||
|
|
gens = get(BASE_A, "/api/generators")["items"]
|
||
|
|
check("Generator-Katalog 15", len(gens) == 15, "%d" % len(gens))
|
||
|
|
gen_ids = [g["id"] for g in gens]
|
||
|
|
check("Ball + Snow vorhanden", "ball" in gen_ids and "snow" in gen_ids)
|
||
|
|
|
||
|
|
r = post(BASE_A, "/api/layers/add-generator", {"generator": "ball"})
|
||
|
|
check("Generator-Layer ball", r.get("layer", {}).get("kind")
|
||
|
|
== "generator", r)
|
||
|
|
gid = r.get("layer", {}).get("id")
|
||
|
|
r = post(BASE_A, "/api/layers/update",
|
||
|
|
{"id": gid, "fx1": "blur", "fx1_intensity": 0.6})
|
||
|
|
check("Generator + FX kombiniert", r.get("layer", {}).get("fx1")
|
||
|
|
== "blur", r)
|
||
|
|
time.sleep(2)
|
||
|
|
st = get(BASE_A, "/api/status")
|
||
|
|
check("Engine rendert Generator+FX", st["frames_rendered"] > 0
|
||
|
|
and st["error"] is None,
|
||
|
|
"frames=%d err=%s" % (st["frames_rendered"], st["error"]))
|
||
|
|
|
||
|
|
# Generator wechseln (Pattern-Tausch ohne neuen Layer)
|
||
|
|
r = post(BASE_A, "/api/layers/update", {"id": gid, "name": "snow"})
|
||
|
|
check("Generator-Wechsel ball->snow", r.get("layer", {}).get("name")
|
||
|
|
== "snow", r)
|
||
|
|
time.sleep(1.5)
|
||
|
|
st = get(BASE_A, "/api/status")
|
||
|
|
check("Engine nach Generator-Wechsel", st["running"] is True,
|
||
|
|
"err=%s" % st["error"])
|
||
|
|
|
||
|
|
# Ungueltiger Generator abgewiesen
|
||
|
|
r = post(BASE_A, "/api/layers/add-generator", {"generator": "keins"})
|
||
|
|
check("Ungueltiger Generator 400", "error" in r, r)
|
||
|
|
|
||
|
|
# 2) Remote-Server-Verteilung: A registriert B
|
||
|
|
r = post(BASE_A, "/api/servers/add",
|
||
|
|
{"name": "Node-B", "url": BASE_B})
|
||
|
|
check("Server B registriert", r.get("added") is True, r)
|
||
|
|
|
||
|
|
# Duplikat-Name abgewiesen
|
||
|
|
r = post(BASE_A, "/api/servers/add",
|
||
|
|
{"name": "Node-B", "url": BASE_B})
|
||
|
|
check("Duplikat-Name abgewiesen", "error" in r, r)
|
||
|
|
|
||
|
|
# Ungueltige URL abgewiesen
|
||
|
|
r = post(BASE_A, "/api/servers/add",
|
||
|
|
{"name": "X", "url": "ftp://kein-http"})
|
||
|
|
check("Nicht-HTTP abgewiesen", "error" in r, r)
|
||
|
|
|
||
|
|
# Server-Liste: B ist online und liefert Medien
|
||
|
|
srv = get(BASE_A, "/api/servers")["servers"]
|
||
|
|
check("Server-Liste 1 Eintrag", len(srv) == 1, srv)
|
||
|
|
check("Node-B online", srv[0]["online"] is True, srv[0]["name"])
|
||
|
|
check("B-Medien sichtbar",
|
||
|
|
any(m["name"] == "clip_vom_server_B.mp4"
|
||
|
|
for m in srv[0]["items"]),
|
||
|
|
[m["name"] for m in srv[0]["items"]])
|
||
|
|
|
||
|
|
# Medium von B nach A ziehen
|
||
|
|
r = post(BASE_A, "/api/servers/import",
|
||
|
|
{"server": "Node-B", "media": "clip_vom_server_B.mp4"})
|
||
|
|
check("Import von B nach A (Originalname, getrennte Ordner)",
|
||
|
|
r.get("saved") == "clip_vom_server_B.mp4", r)
|
||
|
|
# Datei-Gleichheit pruefen (SHA von Quelle und Kopie)
|
||
|
|
import hashlib
|
||
|
|
def sha(path):
|
||
|
|
return hashlib.sha256(open(path, "rb").read()).hexdigest()[:16]
|
||
|
|
src = os.path.join(DATA_B, "media", "clip_vom_server_B.mp4")
|
||
|
|
dst = os.path.join(ROOT, "media", "clip_vom_server_B.mp4")
|
||
|
|
check("Importierte Datei byte-identisch", sha(src) == sha(dst),
|
||
|
|
"sha=%s" % sha(dst))
|
||
|
|
lib_a = get(BASE_A, "/api/media")["items"]
|
||
|
|
check("Datei liegt in A-Archiv",
|
||
|
|
any(m["name"] == "clip_vom_server_B.mp4" for m in lib_a),
|
||
|
|
[m["name"] for m in lib_a])
|
||
|
|
# Import in A als Layer nutzbar
|
||
|
|
r = post(BASE_A, "/api/layers/add",
|
||
|
|
{"name": "clip_vom_server_B.mp4"})
|
||
|
|
check("Importiertes als Layer", "layer" in r, r)
|
||
|
|
time.sleep(1.5)
|
||
|
|
st = get(BASE_A, "/api/status")
|
||
|
|
check("Engine rendert mit Import", st["frames_rendered"] > 0
|
||
|
|
and st["error"] is None, "frames=%d" % st["frames_rendered"])
|
||
|
|
|
||
|
|
# Import von unregistriertem Namen -> 404
|
||
|
|
r = post(BASE_A, "/api/servers/import",
|
||
|
|
{"server": "gibts_nicht", "media": "x.mp4"})
|
||
|
|
check("Unbekannter Server 404", "error" in r, r)
|
||
|
|
|
||
|
|
# Offline-Server: B stoppen, Liste muss offline zeigen
|
||
|
|
procs["B"].terminate()
|
||
|
|
procs["B"].wait(timeout=6)
|
||
|
|
time.sleep(0.5)
|
||
|
|
srv = get(BASE_A, "/api/servers")["servers"]
|
||
|
|
check("B nach Stop offline", srv[0]["online"] is False)
|
||
|
|
r = post(BASE_A, "/api/servers/remove", {"name": "Node-B"})
|
||
|
|
check("Server entfernt", r.get("removed") is True, r)
|
||
|
|
cfg = json.load(open(os.path.join(ROOT, "config.json")))
|
||
|
|
check("config.json Server entfernt", cfg.get("servers") == [])
|
||
|
|
|
||
|
|
# 3) Autostart + Settings-Merge: Autostart darf beim
|
||
|
|
# Speichern der Einstellungen NICHT verloren gehen
|
||
|
|
post(BASE_A, "/api/projects/save", {"name": "Autostart Test"})
|
||
|
|
r = post(BASE_A, "/api/projects/autostart", {"name": "Autostart Test"})
|
||
|
|
check("Autostart gesetzt", r.get("autostart") == "Autostart Test", r)
|
||
|
|
# Einstellungen OHNE autostart_project speichern (UI-Merge-Fall)
|
||
|
|
r = post(BASE_A, "/api/settings", {"preview": {"fps": 20}})
|
||
|
|
check("Settings gespeichert (Merge)", bool(r.get("saved")), r)
|
||
|
|
cfg = json.load(open(os.path.join(ROOT, "config.json")))
|
||
|
|
check("Autostart UEBERLEBT Settings-Save",
|
||
|
|
cfg["engine"]["autostart_project"] == "Autostart Test",
|
||
|
|
cfg["engine"]["autostart_project"])
|
||
|
|
check("FPS uebernommen", cfg["preview"]["fps"] == 20)
|
||
|
|
st = get(BASE_A, "/api/status")
|
||
|
|
check("Server laeuft weiter", st["running"] is True)
|
||
|
|
|
||
|
|
# Autostart via Settings-Endpunkt abwaehlen (NULL akzeptiert)
|
||
|
|
r = post(BASE_A, "/api/settings",
|
||
|
|
{"engine": {"autostart_project": None}})
|
||
|
|
check("Autostart via Settings abgewaehlt",
|
||
|
|
bool(r.get("saved"))
|
||
|
|
and json.load(open(os.path.join(
|
||
|
|
ROOT, "config.json")))["engine"]["autostart_project"] is None)
|
||
|
|
|
||
|
|
# Generatoren in Projekten: speichern + laden
|
||
|
|
post(BASE_A, "/api/layers/add-generator", {"generator": "gradient"})
|
||
|
|
r = post(BASE_A, "/api/projects/save", {"name": "Mit Generator"})
|
||
|
|
check("Projekt mit Generator gespeichert", r.get("saved")
|
||
|
|
== "Mit Generator", r)
|
||
|
|
r = post(BASE_A, "/api/projects/load", {"name": "Mit Generator"})
|
||
|
|
check("Projekt mit Generator geladen", r.get("loaded") is True, r)
|
||
|
|
st = get(BASE_A, "/api/status")
|
||
|
|
check("Generator nach Projekt-Load aktiv",
|
||
|
|
any(l["kind"] == "generator" and l["name"] == "gradient"
|
||
|
|
for l in st["layers"]),
|
||
|
|
[(l["kind"], l["name"]) for l in st["layers"]])
|
||
|
|
|
||
|
|
print("")
|
||
|
|
print("ALLE VERTEILUNGS/GENERATOR-E2E-TESTS BESTANDEN")
|
||
|
|
finally:
|
||
|
|
stop_all()
|