362e089be0
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
173 lines
6.6 KiB
Python
173 lines
6.6 KiB
Python
"""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"
|