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)
|
display_manager = VisualTestDisplayManager(width=width, height=height)
|
||||||
cache_manager = MockCacheManager()
|
cache_manager = MockCacheManager()
|
||||||
plugin_manager = MockPluginManager()
|
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:
|
if not args.json:
|
||||||
print(f"Vegas audit — display {width}x{height}, scroll {speed:g}px/s, "
|
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:
|
if self.min_plugin_width < 0:
|
||||||
errors.append(
|
errors.append(
|
||||||
f"min_plugin_width must be >= 0, got {self.min_plugin_width}")
|
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:
|
if self.lead_in_width < 0:
|
||||||
errors.append(
|
errors.append(
|
||||||
|
|||||||
@@ -398,6 +398,82 @@ class TestStreamGrouping:
|
|||||||
assert len(sm.get_all_content_for_composition()) == 5
|
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:
|
class TestCycleSizing:
|
||||||
def test_plugins_per_cycle_defaults_above_buffer_ahead(self):
|
def test_plugins_per_cycle_defaults_above_buffer_ahead(self):
|
||||||
cfg = VegasModeConfig()
|
cfg = VegasModeConfig()
|
||||||
|
|||||||
@@ -961,13 +961,21 @@ def save_main_config():
|
|||||||
}), 400
|
}), 400
|
||||||
vegas_config['max_plugin_width_ratio'] = ratio
|
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 = {
|
numeric_fields = {
|
||||||
'vegas_scroll_speed': ('scroll_speed', 1, 100),
|
'vegas_scroll_speed': ('scroll_speed', 1, 200),
|
||||||
'vegas_separator_width': ('separator_width', 0, 500),
|
'vegas_separator_width': ('separator_width', 0, 128),
|
||||||
'vegas_intra_plugin_gap': ('intra_plugin_gap', 0, 128),
|
'vegas_intra_plugin_gap': ('intra_plugin_gap', 0, 128),
|
||||||
'vegas_target_fps': ('target_fps', 1, 200),
|
'vegas_target_fps': ('target_fps', 30, 200),
|
||||||
'vegas_buffer_ahead': ('buffer_ahead', 1, 20),
|
'vegas_buffer_ahead': ('buffer_ahead', 1, 5),
|
||||||
'vegas_trim_threshold': ('trim_threshold', 0, 254),
|
'vegas_trim_threshold': ('trim_threshold', 0, 254),
|
||||||
'vegas_content_padding': ('content_padding', 0, 128),
|
'vegas_content_padding': ('content_padding', 0, 128),
|
||||||
'vegas_min_plugin_width': ('min_plugin_width', 0, 512),
|
'vegas_min_plugin_width': ('min_plugin_width', 0, 512),
|
||||||
|
|||||||
Reference in New Issue
Block a user