0922cc1d68
- Struktur gemäß §8 (Eigentumsgrenzen), PLAN.md als normative Basis - Pflichtdokumente: STATUS.md, ERRORS.md, TEST_REPORT.md, CHANGELOG.md, ADRs - ADR-0001 Python 3.13-Pin, ADR-0002 GStreamer 1.28.6-Pin (Windows), ADR-0003 IPC TCP+MessagePack v1 - Kernpakete: hms_protocol, hms_domain, hms_parameter, hms_artnet, hms_adaptive, hms_capabilities, hms_plugin_sdk - Renderer-Spike: D3D11-Primärpfad + Dev-GL-Pfad (§36 Nr. 4-5) - Control Core: FastAPI REST + WebSocket (§36 Nr. 9) - Beispielplugins: Passthrough + Gaussian Blur (3 Adaptive-Quality- Varianten, HLSL/GLSL/GLES) - Tools: Art-Net-Emulator, Fixture-Generator (Master32/Layer64-CSV), Capability-Probe - JSON-Schemas: IPC, Plugin, Projekt, Cluster - 121 Unit-/Integrationstests grün, Ruff grün Gate 0 bleibt offen: Hardwaremessungen nur auf echter Windows-Referenz- hardware gültig (§29.7, §33).
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))
|