Files

122 lines
4.0 KiB
Python
Raw Permalink Normal View History

from dataclasses import dataclass
import pytest
from openrgb_hue.animation import (
ANIMATION_NAMES,
PingpongAnimation,
PulseAnimation,
ScrollAnimation,
StaticAnimation,
UnknownAnimationModeError,
WaveAnimation,
build_animation,
run_animation,
)
from openrgb_hue.color import Color
from openrgb_hue.gradient import Gradient
from openrgb_hue.mapping import SequenceMapping, base_positions
from openrgb_hue.targets import synthetic_targets
def test_static_animation_never_moves():
anim = StaticAnimation()
params = anim.frame_params(elapsed=100.0)
assert params.t_offset == 0.0
assert params.brightness == 1.0
assert params.brightness_fn is None
def test_scroll_animation_offset_formula():
anim = ScrollAnimation(speed=0.5) # half a cycle per second
assert anim.frame_params(0.0).t_offset == pytest.approx(0.0)
assert anim.frame_params(1.0).t_offset == pytest.approx(0.5)
assert anim.frame_params(2.0).t_offset == pytest.approx(0.0) # wraps
def test_pingpong_animation_reverses_at_boundary():
anim = PingpongAnimation(speed=1.0)
assert anim.frame_params(0.0).t_offset == pytest.approx(0.0)
assert anim.frame_params(1.0).t_offset == pytest.approx(1.0) # peak
assert anim.frame_params(1.5).t_offset == pytest.approx(0.5) # reversing
assert anim.frame_params(2.0).t_offset == pytest.approx(0.0) # trough
def test_pulse_animation_brightness_bounds():
anim = PulseAnimation(speed=1.0, min_brightness=0.1)
values = [anim.frame_params(t / 8).brightness for t in range(9)]
assert max(values) == pytest.approx(1.0, abs=1e-6)
assert min(values) == pytest.approx(0.1, abs=1e-6)
def test_wave_animation_has_per_led_brightness_fn():
anim = WaveAnimation(speed=0.0, min_brightness=0.0, wavelength=1.0)
params = anim.frame_params(elapsed=0.0)
assert params.brightness_fn is not None
# At elapsed=0, brightness_fn(t) is a sine over t itself.
assert params.brightness_fn(0.0) == pytest.approx(0.5, abs=1e-6)
assert params.brightness_fn(0.25) == pytest.approx(1.0, abs=1e-6)
def test_build_animation_unknown_name_raises():
with pytest.raises(UnknownAnimationModeError):
build_animation("not-a-real-mode")
def test_build_animation_covers_all_names():
for name in ANIMATION_NAMES:
assert build_animation(name).name == name
@dataclass
class FakeColor:
red: int
green: int
blue: int
class FakeDevice:
def __init__(self, n_leds: int):
self.colors = [FakeColor(0, 0, 0) for _ in range(n_leds)]
self.calls: list[tuple[list, bool]] = []
def set_colors(self, colors, fast=False):
self.calls.append((list(colors), fast))
self.colors = colors
class FakeClient:
def __init__(self, n_leds: int = 4):
self.ee_devices = [FakeDevice(n_leds)]
def test_run_animation_terminates_and_restores_on_duration():
leds = synthetic_targets(4)
client = FakeClient(n_leds=4)
gradient = Gradient.from_colors([Color(255, 0, 0), Color(0, 0, 255)], interpolation="rgb")
base = base_positions(SequenceMapping(), leds)
anim = ScrollAnimation(speed=1.0)
run_animation(client, base, gradient, anim, fps=50, duration=0.1, restore=True)
device = client.ee_devices[0]
assert len(device.calls) > 0
# Final write should be the restore call, putting LEDs back to black.
final_colors, final_fast = device.calls[-1]
assert final_fast is True
assert all((c.red, c.green, c.blue) == (0, 0, 0) for c in final_colors)
def test_run_animation_no_restore_leaves_last_frame():
leds = synthetic_targets(4)
client = FakeClient(n_leds=4)
gradient = Gradient.from_colors([Color(255, 0, 0)], interpolation="rgb")
base = base_positions(SequenceMapping(), leds)
anim = StaticAnimation()
run_animation(client, base, gradient, anim, fps=50, duration=0.05, restore=False)
device = client.ee_devices[0]
final_colors, _ = device.calls[-1]
assert all((c.red, c.green, c.blue) == (255, 0, 0) for c in final_colors)