Phase 3: Alle 24 Pflicht-Plugins (10 Generatoren + 14 Filter, §15.1/§15.2)
- 10 Generatoren mit Manifest + HLSL + GLSL + GLES je Plugin: solid, gradient, checker_grid, stripes_chaser, noise_clouds (AQ Octaves 2/3/5), plasma, wave_bars, shapes, drops_ripples, starfield (AQ 32/128/512 Partikel) - 14 Filter mit Manifest + Shader je Backend: transform2d (keine AQ-Geometriereduktion), color_adjust, gradient_map, blur_sharpen (separabel 2-Pass, AQ 5/9/17 Samples), pixelate_quantize, mirror_tile, kaleidoscope, wave_displace (Noise adaptiv, Amount unverändert), rgb_split, glow_bloom (Kaskade 2/4/8), edge_emboss, vignette, strobe_pulse (Safety clamp 2 Hz, nie durch DMX allein freischaltbar), feedback_trails (Double-Buffer, Clear-Trigger, §15.3) - Uniform-Vertrag §14.4 in allen 75 Shadern; Generatoren sampeln nie u_input_texture; alle Filter mit mix-0-Bypass (§15.3) - 195 parametrisierte Plugin-Tests: Manifest/Backends/DMX-Slots<=8/ AQ>=3 Varianten/Uniform-Satz/Vollstaendigkeit exakt 10+14 - Nachtraegliche Manifest-Fixes: AQ-Bloecke fuer 4 1-Pass-Filter, mix-Parameter fuer feedback_trails (Slot-Overlap vermieden) - Gesamtsuite 506 gruen, Ruff gruen
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user