Full rearchitecture using Claude
- event bus systen - all components are independent - preparation for web frontend
This commit is contained in:
141
python-backend/tests/test_effects.py
Normal file
141
python-backend/tests/test_effects.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Golden-byte tests for the LED effect payloads.
|
||||
|
||||
These pin the exact layout the firmware reads back into its C++ structs. If one of
|
||||
them fails, either the firmware struct changed or an effect field was reordered - both
|
||||
would otherwise show up only as garbled LEDs on the real device.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from musicmouse.color import ColorHSV, ColorRGBW
|
||||
from musicmouse.effects import (
|
||||
EffectAlexaSwipeConfig,
|
||||
EffectCircularConfig,
|
||||
EffectRandomTwoColorInterpolationConfig,
|
||||
EffectReverseSwipe,
|
||||
EffectStaticConfig,
|
||||
EffectStaticDetailedConfig,
|
||||
EffectSwipeAndChange,
|
||||
LedEffect,
|
||||
)
|
||||
|
||||
|
||||
def test_color_rgbw_is_four_bytes() -> None:
|
||||
assert ColorRGBW(1.0, 0.5, 0.0, 0.25).as_bytes().hex() == "ff7f003f"
|
||||
|
||||
|
||||
def test_color_rgbw_rejects_out_of_range_channels() -> None:
|
||||
with pytest.raises(ValueError, match=r"within 0\.\.1"):
|
||||
ColorRGBW(1.5, 0, 0, 0).as_bytes()
|
||||
|
||||
|
||||
def test_color_hsv_is_three_floats() -> None:
|
||||
# h=180.0, s=1.0, v=0.5
|
||||
assert ColorHSV(180.0, 1.0, 0.5).as_bytes().hex() == "00003443" "0000803f" "0000003f"
|
||||
|
||||
|
||||
def test_color_hsv_from_rgb() -> None:
|
||||
assert ColorHSV.from_rgb(ColorRGBW(1, 0, 0, 0)) == ColorHSV(0.0, 1.0, 1.0)
|
||||
assert ColorHSV.from_rgb(ColorRGBW(0, 1, 0, 0)) == ColorHSV(120.0, 1.0, 1.0)
|
||||
|
||||
|
||||
def test_static() -> None:
|
||||
effect = EffectStaticConfig(ColorRGBW(1.0, 0.5, 0.0, 0.25), begin=3, end=45)
|
||||
assert effect.as_bytes().hex() == "ff7f003f" "0300" "2d00"
|
||||
|
||||
|
||||
def test_static_detailed() -> None:
|
||||
effect = EffectStaticDetailedConfig(
|
||||
ColorRGBW(0, 0, 0, 1.0), increment=4, begin=0.25, end=0.75, transition_time_in_ms=500
|
||||
)
|
||||
assert effect.as_bytes().hex() == (
|
||||
"000000ff" # color
|
||||
"0400" # increment (uint16)
|
||||
"0000803e" # begin 0.25f
|
||||
"0000403f" # end 0.75f
|
||||
"0000fa43" # transition 500.0f
|
||||
)
|
||||
|
||||
|
||||
def test_circular() -> None:
|
||||
effect = EffectCircularConfig(speed=360, width=180, color=ColorRGBW(0, 0, 1, 0))
|
||||
assert effect.as_bytes().hex() == "0000b443" "00003443" "0000ff00"
|
||||
|
||||
|
||||
def test_reverse_swipe() -> None:
|
||||
effect = EffectReverseSwipe(swipe_speed=720, bell_curve_width_in_leds=3, start_position=180)
|
||||
assert effect.as_bytes().hex() == "00003444" "00004040" "00003443"
|
||||
|
||||
|
||||
def test_alexa_swipe() -> None:
|
||||
effect = EffectAlexaSwipeConfig(
|
||||
primary_color_width=180,
|
||||
transition_width=180,
|
||||
swipe_speed=720,
|
||||
bell_curve_width_in_leds=3,
|
||||
start_position=180,
|
||||
forward=False,
|
||||
primary_color=ColorRGBW(1, 0, 0, 0),
|
||||
secondary_color=ColorRGBW(0, 1, 0, 0),
|
||||
)
|
||||
assert effect.as_bytes().hex() == (
|
||||
"00003443" "00003443" "00003444" "00004040" "00003443" # five floats
|
||||
"00" # forward = false
|
||||
"ff000000" # primary
|
||||
"00ff0000" # secondary
|
||||
)
|
||||
|
||||
|
||||
def test_random_two_color_interpolation() -> None:
|
||||
effect = EffectRandomTwoColorInterpolationConfig(
|
||||
cycle_durations_ms=1000,
|
||||
start_with_existing=True,
|
||||
num_segments=3,
|
||||
hue1_random=False,
|
||||
hue2_random=True,
|
||||
color1=ColorHSV(180.0, 1.0, 0.5),
|
||||
color2=ColorHSV(0.0, 0.0, 0.0),
|
||||
)
|
||||
assert effect.as_bytes().hex() == (
|
||||
"e8030000" # cycle_durations_ms int32
|
||||
"01" # start_with_existing
|
||||
"03000000" # num_segments int32
|
||||
"00" # hue1_random
|
||||
"01" # hue2_random
|
||||
"000034430000803f0000003f" # color1 hsv
|
||||
"000000000000000000000000" # color2 hsv
|
||||
)
|
||||
|
||||
|
||||
def test_rgb_colors_are_converted_to_hsv_on_the_wire() -> None:
|
||||
"""The firmware struct is HSV; reactions hand it RGBW figure colours."""
|
||||
as_rgb = EffectRandomTwoColorInterpolationConfig(
|
||||
color1=ColorRGBW(1, 0, 0, 0), color2=ColorRGBW(0, 1, 0, 0)
|
||||
)
|
||||
as_hsv = EffectRandomTwoColorInterpolationConfig(
|
||||
color1=ColorHSV(0.0, 1.0, 1.0), color2=ColorHSV(120.0, 1.0, 1.0)
|
||||
)
|
||||
assert as_rgb.as_bytes() == as_hsv.as_bytes()
|
||||
|
||||
|
||||
def test_swipe_and_change_is_the_two_payloads_concatenated() -> None:
|
||||
effect = EffectSwipeAndChange()
|
||||
assert effect.as_bytes() == effect.swipe.as_bytes() + effect.change.as_bytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("effect", "size"),
|
||||
[
|
||||
(EffectStaticConfig(ColorRGBW(0, 0, 0, 0)), 8),
|
||||
(EffectStaticDetailedConfig(ColorRGBW(0, 0, 0, 0)), 18),
|
||||
(EffectCircularConfig(), 12),
|
||||
(EffectAlexaSwipeConfig(), 29),
|
||||
(EffectRandomTwoColorInterpolationConfig(), 35),
|
||||
(EffectReverseSwipe(), 12),
|
||||
(EffectSwipeAndChange(), 64),
|
||||
],
|
||||
)
|
||||
def test_payload_sizes(effect: LedEffect, size: int) -> None:
|
||||
assert len(effect.as_bytes()) == size
|
||||
Reference in New Issue
Block a user