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).
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""Unit-Tests Plugin-Manifest-Validierung (PLAN.md §14, §27.2)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from hms_plugin_sdk import load_manifest, validate_manifest, validate_plugin_zip
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
EXAMPLES = REPO / "plugins" / "examples"
|
|
|
|
|
|
def _valid_manifest() -> dict:
|
|
return json.loads(
|
|
(EXAMPLES / "com.hms.fx.example_passthrough" / "plugin.json").read_text(encoding="utf-8")
|
|
)
|
|
|
|
|
|
def test_example_passthrough_validates_with_shaders() -> None:
|
|
manifest, errors = load_manifest(EXAMPLES / "com.hms.fx.example_passthrough")
|
|
assert errors == [], errors
|
|
assert manifest["id"] == "com.hms.fx.example_passthrough"
|
|
|
|
|
|
def test_example_gaussian_blur_validates_with_shaders() -> None:
|
|
manifest, errors = load_manifest(EXAMPLES / "com.hms.fx.gaussian_blur")
|
|
assert errors == [], errors
|
|
variants = manifest["adaptive_quality"]["variants"]
|
|
assert [v["id"] for v in variants] == ["low", "medium", "high"]
|
|
assert [v["samples"] for v in variants] == [5, 9, 17]
|
|
|
|
|
|
def test_valid_manifest_without_root_ok() -> None:
|
|
assert validate_manifest(_valid_manifest()) == []
|
|
|
|
|
|
def test_wrong_schema_version_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["schema_version"] = 99
|
|
assert any("schema_version" in e for e in validate_manifest(m))
|
|
|
|
|
|
def test_invalid_plugin_id_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["id"] = "../evil"
|
|
assert any("invalid plugin id" in e for e in validate_manifest(m))
|
|
|
|
|
|
def test_invalid_semver_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["version"] = "1.0"
|
|
assert any("semantic" in e for e in validate_manifest(m))
|
|
|
|
|
|
def test_dmx_footprint_over_8_slots_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["parameters"] = [
|
|
{"id": f"p{i}", "label": f"P{i}", "type": "float", "minimum": 0, "maximum": 1,
|
|
"default": 0, "dmx_slots": [i]}
|
|
for i in range(1, 10)
|
|
]
|
|
assert any("exceeds 8" in e for e in validate_manifest(m)) # §14.7
|
|
|
|
|
|
def test_unsafe_shader_path_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["entrypoints"]["gl"]["passes"][0]["fragment"] = "../../evil.frag"
|
|
assert any("unsafe shader path" in e for e in validate_manifest(m))
|
|
|
|
|
|
def test_missing_shader_file_detected_with_root() -> None:
|
|
m = _valid_manifest()
|
|
m["entrypoints"]["gl"]["passes"][0]["fragment"] = "shaders/gl/missing.frag"
|
|
errors = validate_manifest(m, plugin_root=EXAMPLES / "com.hms.fx.example_passthrough")
|
|
assert any("missing shader file" in e for e in errors)
|
|
|
|
|
|
def test_invalid_failure_mode_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["failure_mode"] = "crash"
|
|
assert any("failure_mode" in e for e in validate_manifest(m))
|
|
|
|
|
|
def test_duplicate_parameter_ids_rejected() -> None:
|
|
m = _valid_manifest()
|
|
m["parameters"].append(dict(m["parameters"][0]))
|
|
assert any("duplicate parameter id" in e for e in validate_manifest(m))
|
|
|
|
|
|
def _make_zip(tmp_path: Path, files: dict[str, str | bytes]) -> Path:
|
|
zpath = tmp_path / "plugin.zip"
|
|
with zipfile.ZipFile(zpath, "w") as zf:
|
|
for name, content in files.items():
|
|
zf.writestr(name, content)
|
|
return zpath
|
|
|
|
|
|
def test_valid_zip_passes(tmp_path: Path) -> None:
|
|
plugin_dir = EXAMPLES / "com.hms.fx.example_passthrough"
|
|
files: dict[str, str | bytes] = {}
|
|
for f in sorted(plugin_dir.rglob("*")):
|
|
if f.is_file():
|
|
rel = f.relative_to(plugin_dir.parent)
|
|
files[str(rel)] = f.read_text(encoding="utf-8")
|
|
zpath = _make_zip(tmp_path, files)
|
|
assert validate_plugin_zip(zpath) == []
|
|
|
|
|
|
def test_zip_traversal_rejected(tmp_path: Path) -> None:
|
|
files = {"pkg/plugin.json": json.dumps(_valid_manifest()), "../evil.frag": "x"}
|
|
zpath = _make_zip(tmp_path, files)
|
|
assert any("unsafe path" in e for e in validate_plugin_zip(zpath))
|
|
|
|
|
|
def test_zip_disallowed_file_type_rejected(tmp_path: Path) -> None:
|
|
files = {
|
|
"pkg/plugin.json": json.dumps(_valid_manifest()),
|
|
"pkg/evil.exe": "MZ",
|
|
}
|
|
zpath = _make_zip(tmp_path, files)
|
|
assert any("disallowed file type" in e for e in validate_plugin_zip(zpath))
|
|
|
|
|
|
def test_zip_without_manifest_rejected(tmp_path: Path) -> None:
|
|
zpath = _make_zip(tmp_path, {"pkg/shader.frag": "void main(){}"})
|
|
assert any("plugin.json not found" in e for e in validate_plugin_zip(zpath))
|
|
|
|
|
|
def test_corrupt_zip_rejected(tmp_path: Path) -> None:
|
|
zpath = tmp_path / "broken.zip"
|
|
zpath.write_bytes(b"not a zip at all")
|
|
assert any("not a valid zip" in e for e in validate_plugin_zip(zpath))
|