Files
hms-mediaengine/tests/unit/test_builtin_plugins.py
T

173 lines
6.6 KiB
Python
Raw Normal View History

"""Tests über ALLE eingebauten Pflicht-Plugins (PLAN.md §15, §29.1).
Parametrisiert über jedes gefundene Builtin-Plugin:
- Manifest validiert (inkl. Shader-Existenz je Backend)
- kind stimmt zur Kategorie (generator/filter)
- alle drei Backends d3d11/gl/gles deklariert (§12.6 Backend-Vertrag)
- max. 8 DMX-Slots gesamt (§14.7)
- adaptive_quality mit mindestens 3 Varianten (§15.1/§15.2 AQ-Regeln)
- Standard-Uniform-Satz in jedem Shader deklariert (§14.4)
Vollständigkeitsprüfung (§15.1/§15.2): exakt 10 Generatoren und
14 Filter mit den normativen Plugin-IDs sind vorhanden.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from hms_plugin_sdk import load_manifest
REPO = Path(__file__).resolve().parents[2]
BUILTIN = REPO / "plugins" / "builtin"
REQUIRED_GENERATORS = {
"com.hms.generator.solid",
"com.hms.generator.gradient",
"com.hms.generator.checker_grid",
"com.hms.generator.stripes_chaser",
"com.hms.generator.noise_clouds",
"com.hms.generator.plasma",
"com.hms.generator.wave_bars",
"com.hms.generator.shapes",
"com.hms.generator.drops_ripples",
"com.hms.generator.starfield",
}
REQUIRED_FILTERS = {
"com.hms.fx.transform2d",
"com.hms.fx.color_adjust",
"com.hms.fx.gradient_map",
"com.hms.fx.blur_sharpen",
"com.hms.fx.pixelate_quantize",
"com.hms.fx.mirror_tile",
"com.hms.fx.kaleidoscope",
"com.hms.fx.wave_displace",
"com.hms.fx.rgb_split",
"com.hms.fx.glow_bloom",
"com.hms.fx.edge_emboss",
"com.hms.fx.vignette",
"com.hms.fx.strobe_pulse",
"com.hms.fx.feedback_trails",
}
def _plugin_dirs() -> list[Path]:
dirs: list[Path] = []
for category in ("generators", "filters"):
dirs.extend(sorted((BUILTIN / category).glob("com.*")))
return dirs
def _shaders_of(plugin_dir: Path) -> list[Path]:
return [p for p in (plugin_dir / "shaders").rglob("*") if p.is_file()]
# ---------- Vollständigkeit (§15.1, §15.2) ----------
def test_all_mandatory_generators_present() -> None:
found = {d.name for d in _plugin_dirs()}
missing = REQUIRED_GENERATORS - found
assert not missing, f"fehlende Pflicht-Generatoren: {sorted(missing)}"
def test_all_mandatory_filters_present() -> None:
found = {d.name for d in _plugin_dirs()}
missing = REQUIRED_FILTERS - found
assert not missing, f"fehlende Pflicht-Filter: {sorted(missing)}"
def test_exactly_the_mandatory_set() -> None:
"""Keine unautorisierten Extras im Pflicht-Verzeichnis (Verwechselungs-
schutz; Beispiele leben in plugins/examples)."""
found = {d.name for d in _plugin_dirs()}
expected = REQUIRED_GENERATORS | REQUIRED_FILTERS
assert found == expected, (
f"unerwartete Differenz: extra={sorted(found - expected)} "
f"fehlt={sorted(expected - found)}"
)
# ---------- Parametrisierte Einzelprüfungen ----------
@pytest.mark.parametrize("plugin_dir", _plugin_dirs(), ids=lambda p: p.name)
class TestBuiltinPlugin:
def test_manifest_validates(self, plugin_dir: Path) -> None:
manifest, errors = load_manifest(plugin_dir)
assert errors == [], errors
assert manifest["vendor"] == "HMS"
assert manifest["failure_mode"] == "bypass"
assert manifest["schema_version"] == 1
assert manifest["api_version"] == 1
def test_kind_matches_category(self, plugin_dir: Path) -> None:
manifest, _ = load_manifest(plugin_dir)
category = plugin_dir.parent.name
expected = "generator" if category == "generators" else "filter"
assert manifest["kind"] == expected
def test_all_backends_declared(self, plugin_dir: Path) -> None:
"""§12.6 Backend-Vertrag: V1-Plugins liefern d3d11, gl und gles."""
manifest, _ = load_manifest(plugin_dir)
assert set(manifest["entrypoints"]) == {"d3d11", "gl", "gles"}
assert set(manifest["capabilities"]["supported_backends"]) == {
"d3d11",
"gl",
"gles",
}
def test_dmx_slot_footprint_max_8(self, plugin_dir: Path) -> None:
"""§14.7: maximal 8 generische DMX-Slots je Plugin."""
manifest, _ = load_manifest(plugin_dir)
total = sum(len(p.get("dmx_slots", [])) for p in manifest["parameters"])
assert total <= 8, f"{plugin_dir.name}: {total} DMX-Slots > 8"
def test_adaptive_quality_variants(self, plugin_dir: Path) -> None:
"""§15.1/§15.2: AQ mit mindestens 3 Varianten; Parametersemantik
bleibt über Varianten unverändert (semantic_parameters_unchanged
darf nur AQ-interne Größen nennen, nie semantische Parameter wegnehmen)."""
manifest, _ = load_manifest(plugin_dir)
aq = manifest.get("adaptive_quality")
assert aq is not None, "adaptive_quality fehlt"
ids = [v["id"] for v in aq["variants"]]
assert len(ids) >= 3, f"nur {len(ids)} AQ-Varianten"
def test_shader_uniform_contract(self, plugin_dir: Path) -> None:
"""§14.4: Standard-Inputs sind in jedem Shader deklariert; Filter
sampeln die Eingabetextur, Generatoren deklarieren sie (Uniform-Satz
identisch), auch wenn sie sie nicht nutzen."""
shaders = _shaders_of(plugin_dir)
assert shaders, "keine Shader-Dateien"
for shader in shaders:
src = shader.read_text(encoding="utf-8")
for uniform in ("u_resolution", "u_time_seconds", "u_layer_opacity"):
assert uniform in src, f"{shader.name}: {uniform} fehlt"
def test_generator_shader_has_no_input_sampling(self, plugin_dir: Path) -> None:
"""Generatoren erzeugen Inhalte ohne Eingangsbild (§12.3): der
Shader darf die Eingabetextur nicht als Quelle sampeln."""
manifest, _ = load_manifest(plugin_dir)
if manifest["kind"] != "generator":
return
for shader in _shaders_of(plugin_dir):
src = shader.read_text(encoding="utf-8")
sample_calls = (
src.count("u_input_texture.Sample")
+ src.count("texture(u_input_texture")
+ src.count("texture2D(u_input_texture")
)
assert sample_calls == 0, (
f"{plugin_dir.name}/{shader.name}: Generator sampelt u_input_texture"
)
def test_filter_mix_zero_is_free_bypass(self, plugin_dir: Path) -> None:
"""§15.3: jeder Filter besitzt mix; Mix 0 bypassed kostengünstig."""
manifest, _ = load_manifest(plugin_dir)
if manifest["kind"] != "filter":
return
ids = [p["id"] for p in manifest["parameters"]]
assert "mix" in ids, f"{plugin_dir.name}: mix-Parameter fehlt"