mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
check_plugin.py, render_plugin.py, and the pytest plugin matrix each built
config as {"enabled": True} then merged in config_schema.json's defaults on
top, letting a plugin's own enabled:false default (a reasonable choice for
a seasonal/opt-in plugin -- 15 of 23 real plugins ship one) silently win.
Every harness/CI render of those plugins was testing "disabled, do
nothing" rather than real behavior.
Extract build_full_config() into testing/loading.py (already the shared
home for plugin-discovery/config-default logic) and use it from all three
call sites: schema defaults, then a forced enabled=True, then harness.json's
config, then the caller's explicit config -- so a test can still
deliberately disable a plugin on purpose, it just can't happen by accident
via the plugin's own shipped schema default anymore.
115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
"""
|
|
Shared helpers for loading a plugin headlessly.
|
|
|
|
Used by scripts/render_plugin.py, scripts/check_plugin.py, and the harness so
|
|
plugin discovery / manifest / config-default logic lives in exactly one place.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional, Sequence, Union
|
|
|
|
|
|
def find_plugin_dir(plugin_id: str, search_dirs: Sequence[Union[str, Path]]) -> Optional[Path]:
|
|
"""Find a plugin directory by searching multiple paths."""
|
|
from src.plugin_system.plugin_loader import PluginLoader
|
|
loader = PluginLoader()
|
|
for search_dir in search_dirs:
|
|
search_path = Path(search_dir)
|
|
if not search_path.exists():
|
|
continue
|
|
result = loader.find_plugin_directory(plugin_id, search_path)
|
|
if result:
|
|
return Path(result)
|
|
return None
|
|
|
|
|
|
def load_manifest(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
|
"""Load and return manifest.json from a plugin directory."""
|
|
manifest_path = Path(plugin_dir) / 'manifest.json'
|
|
if not manifest_path.exists():
|
|
raise FileNotFoundError(f"No manifest.json in {plugin_dir}")
|
|
with open(manifest_path, 'r') as f:
|
|
return json.load(f)
|
|
|
|
|
|
def load_config_defaults(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
|
"""Extract default values from a plugin's config_schema.json (empty if none)."""
|
|
schema_path = Path(plugin_dir) / 'config_schema.json'
|
|
if not schema_path.exists():
|
|
return {}
|
|
with open(schema_path, 'r') as f:
|
|
schema = json.load(f)
|
|
defaults: Dict[str, Any] = {}
|
|
for key, prop in schema.get('properties', {}).items():
|
|
if isinstance(prop, dict) and 'default' in prop:
|
|
defaults[key] = prop['default']
|
|
return defaults
|
|
|
|
|
|
def load_harness_spec(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
|
|
"""Optional per-plugin harness settings from <plugin>/test/harness.json.
|
|
|
|
Lets a plugin opt into golden-image testing by declaring how to render it
|
|
deterministically. All keys optional:
|
|
{
|
|
"config": {...}, # config overrides
|
|
"mock_data": "fixtures/mock.json", # path (relative to plugin dir) to cache fixtures
|
|
"freeze_time": "2025-08-01 15:25:00",
|
|
"skip_update": false,
|
|
"fill_check": "warn", # or "strict": underfilled big panels FAIL
|
|
"variants": [ # extra runs with config overlays and
|
|
{ # their own golden dirs — e.g. an
|
|
"name": "adaptive", # opt-in adaptive mode tested beside
|
|
"config": {"layout_mode": "adaptive"}, # the classic default
|
|
"golden_dir": "test/golden-adaptive"
|
|
}
|
|
]
|
|
}
|
|
Returns {} when no harness.json exists.
|
|
"""
|
|
spec_path = Path(plugin_dir) / 'test' / 'harness.json'
|
|
if not spec_path.exists():
|
|
return {}
|
|
with open(spec_path, 'r') as f:
|
|
spec = json.load(f)
|
|
|
|
# Resolve mock_data path and inline its contents for convenience.
|
|
mock_rel = spec.get('mock_data')
|
|
if mock_rel:
|
|
mock_path = Path(plugin_dir) / mock_rel
|
|
if not mock_path.exists():
|
|
# A declared-but-missing fixture is a harness config error: failing
|
|
# loudly beats silently rendering the plugin with no mock data.
|
|
raise FileNotFoundError(
|
|
f"harness.json references mock_data '{mock_rel}' but "
|
|
f"{mock_path} does not exist"
|
|
)
|
|
with open(mock_path, 'r') as mf:
|
|
spec['mock_data_contents'] = json.load(mf)
|
|
return spec
|
|
|
|
|
|
def build_full_config(
|
|
plugin_dir: Union[str, Path],
|
|
spec: Optional[Dict[str, Any]] = None,
|
|
cli_config: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Build the config a plugin sees under test.
|
|
|
|
Merge order: config_schema.json defaults, then a forced ``enabled: True``,
|
|
then harness.json's config overlay, then the caller's explicit config --
|
|
most specific wins. `enabled` is re-asserted *after* the schema defaults
|
|
so a plugin that reasonably ships `enabled: false` (e.g. a seasonal or
|
|
opt-in plugin) can't silently make every harness run test "disabled, do
|
|
nothing" by accident -- callers that genuinely want to test the disabled
|
|
path can still do so via `cli_config={"enabled": False}`.
|
|
"""
|
|
spec = spec or {}
|
|
config: Dict[str, Any] = {}
|
|
config.update(load_config_defaults(plugin_dir))
|
|
config["enabled"] = True
|
|
config.update(spec.get("config", {}))
|
|
config.update(cli_config or {})
|
|
return config
|