mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
Align Vegas API bounds with validate(), fix audit config plumbing
Both from review feedback on #423. The web API's accepted ranges disagreed with VegasModeConfig.validate(), which is what actually gates Vegas starting: scroll_speed 1-100 -> 1-200 (a slider value of 150 returned 400) separator_width 0-500 -> 0-128 target_fps 1-200 -> 30-200 buffer_ahead 1-20 -> 1-5 The three loose ones were the dangerous direction: the value saved with a 200, then VegasModeCoordinator.start() failed validation with only a log line, so the ticker silently never ran. The UI already matched validate() in all four cases, so the API was the odd one out. test_vegas_api_bounds_match_validate parses the numeric_fields map out of api_v3 and asserts every bound against validate(), plus that validate() accepts both endpoints and rejects just outside them, so these cannot drift apart again. That test immediately caught a missing upper bound on min_plugin_width, now added — unbounded it would drop every segment and leave a blank ticker. Separately, vegas_audit.py constructed PluginAdapter without the config, so it fell back to VegasModeConfig() defaults and would report trimming and width-budget behaviour that differed from the user's config.json. It now passes the loaded config exactly as the coordinator does. This is the same class of drift the explicit lead_gap and grouping arguments already guard against. Output is unchanged on a rig whose config matches the defaults. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
@@ -208,7 +208,12 @@ def main() -> int:
|
||||
display_manager = VisualTestDisplayManager(width=width, height=height)
|
||||
cache_manager = MockCacheManager()
|
||||
plugin_manager = MockPluginManager()
|
||||
adapter = PluginAdapter(display_manager)
|
||||
# Pass the loaded config, exactly as VegasModeCoordinator does. Omitting it
|
||||
# makes PluginAdapter fall back to VegasModeConfig() defaults, so the audit
|
||||
# would silently report trimming and width-budget behaviour that differs
|
||||
# from the user's config.json — the same drift the lead_gap and grouping
|
||||
# arguments below exist to avoid.
|
||||
adapter = PluginAdapter(display_manager, vegas)
|
||||
|
||||
if not args.json:
|
||||
print(f"Vegas audit — display {width}x{height}, scroll {speed:g}px/s, "
|
||||
|
||||
@@ -230,6 +230,11 @@ class VegasModeConfig:
|
||||
if self.min_plugin_width < 0:
|
||||
errors.append(
|
||||
f"min_plugin_width must be >= 0, got {self.min_plugin_width}")
|
||||
# Bounded because every segment narrower than this is dropped — an
|
||||
# unbounded value would discard every plugin and leave a blank ticker.
|
||||
if self.min_plugin_width > 512:
|
||||
errors.append(
|
||||
f"min_plugin_width must be <= 512, got {self.min_plugin_width}")
|
||||
|
||||
if self.lead_in_width < 0:
|
||||
errors.append(
|
||||
|
||||
@@ -398,6 +398,82 @@ class TestStreamGrouping:
|
||||
assert len(sm.get_all_content_for_composition()) == 5
|
||||
|
||||
|
||||
class TestApiBoundsMatchValidate:
|
||||
"""
|
||||
The web API's accepted range for each Vegas setting must agree with
|
||||
VegasModeConfig.validate(), which is what actually gates Vegas starting.
|
||||
|
||||
A looser API bound saves a value with a 200 and then makes
|
||||
VegasModeCoordinator.start() bail out with only a log line, so the ticker
|
||||
silently never runs. A tighter one rejects a legitimate value with a 400.
|
||||
Both happened before this test existed.
|
||||
"""
|
||||
|
||||
# (config key, min, max) as validate() enforces them.
|
||||
EXPECTED = {
|
||||
'scroll_speed': (1, 200),
|
||||
'separator_width': (0, 128),
|
||||
'intra_plugin_gap': (0, 128),
|
||||
'target_fps': (30, 200),
|
||||
'buffer_ahead': (1, 5),
|
||||
'trim_threshold': (0, 254),
|
||||
'content_padding': (0, 128),
|
||||
'min_plugin_width': (0, 512),
|
||||
'plugins_per_cycle': (1, 50),
|
||||
}
|
||||
|
||||
def _api_numeric_fields(self):
|
||||
"""Extract the numeric_fields map from api_v3 without importing Flask."""
|
||||
import ast
|
||||
import pathlib
|
||||
src = pathlib.Path('web_interface/blueprints/api_v3.py').read_text()
|
||||
tree = ast.parse(src)
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
|
||||
if 'numeric_fields' not in targets:
|
||||
continue
|
||||
if not isinstance(node.value, ast.Dict):
|
||||
continue
|
||||
found = {}
|
||||
for key, value in zip(node.value.keys, node.value.values):
|
||||
if not isinstance(key, ast.Constant):
|
||||
continue
|
||||
if not str(key.value).startswith('vegas_'):
|
||||
break
|
||||
cfg_key, lo, hi = [ast.literal_eval(e) for e in value.elts]
|
||||
found[cfg_key] = (lo, hi)
|
||||
if found:
|
||||
return found
|
||||
raise AssertionError("could not locate the vegas numeric_fields map")
|
||||
|
||||
def test_every_bound_matches_validate(self):
|
||||
api = self._api_numeric_fields()
|
||||
mismatched = {
|
||||
key: (api[key], expected)
|
||||
for key, expected in self.EXPECTED.items()
|
||||
if key in api and api[key] != expected
|
||||
}
|
||||
assert not mismatched, f"API bounds disagree with validate(): {mismatched}"
|
||||
|
||||
@pytest.mark.parametrize('key,bounds', sorted(EXPECTED.items()))
|
||||
def test_validate_accepts_both_endpoints(self, key, bounds):
|
||||
lo, hi = bounds
|
||||
for value in (lo, hi):
|
||||
cfg = VegasModeConfig(**{key: value})
|
||||
errors = [e for e in cfg.validate() if key in e]
|
||||
assert not errors, f"{key}={value} should be valid, got {errors}"
|
||||
|
||||
@pytest.mark.parametrize('key,bounds', sorted(EXPECTED.items()))
|
||||
def test_validate_rejects_just_outside(self, key, bounds):
|
||||
lo, hi = bounds
|
||||
for value in (lo - 1, hi + 1):
|
||||
cfg = VegasModeConfig(**{key: value})
|
||||
errors = [e for e in cfg.validate() if key in e]
|
||||
assert errors, f"{key}={value} should be rejected"
|
||||
|
||||
|
||||
class TestCycleSizing:
|
||||
def test_plugins_per_cycle_defaults_above_buffer_ahead(self):
|
||||
cfg = VegasModeConfig()
|
||||
|
||||
@@ -961,13 +961,21 @@ def save_main_config():
|
||||
}), 400
|
||||
vegas_config['max_plugin_width_ratio'] = ratio
|
||||
|
||||
# Handle numeric settings with validation
|
||||
# Handle numeric settings with validation.
|
||||
#
|
||||
# These bounds must match VegasModeConfig.validate(), which is what
|
||||
# actually gates Vegas starting. Where they were looser, a value
|
||||
# saved with a 200 and then made VegasModeCoordinator.start() bail
|
||||
# out with only a log line, so the ticker silently never ran.
|
||||
# Where they were tighter (scroll_speed capped at 100 against a
|
||||
# slider that goes to 200), a legitimate value was rejected with a
|
||||
# 400. See test_vegas_api_bounds_match_validate.
|
||||
numeric_fields = {
|
||||
'vegas_scroll_speed': ('scroll_speed', 1, 100),
|
||||
'vegas_separator_width': ('separator_width', 0, 500),
|
||||
'vegas_scroll_speed': ('scroll_speed', 1, 200),
|
||||
'vegas_separator_width': ('separator_width', 0, 128),
|
||||
'vegas_intra_plugin_gap': ('intra_plugin_gap', 0, 128),
|
||||
'vegas_target_fps': ('target_fps', 1, 200),
|
||||
'vegas_buffer_ahead': ('buffer_ahead', 1, 20),
|
||||
'vegas_target_fps': ('target_fps', 30, 200),
|
||||
'vegas_buffer_ahead': ('buffer_ahead', 1, 5),
|
||||
'vegas_trim_threshold': ('trim_threshold', 0, 254),
|
||||
'vegas_content_padding': ('content_padding', 0, 128),
|
||||
'vegas_min_plugin_width': ('min_plugin_width', 0, 512),
|
||||
|
||||
Reference in New Issue
Block a user