Phase 0: Repository-Initialisierung nach Bauplan v1.2
- 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).
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user