"""Tests for calibration system.""" import numpy as np import pytest from wled_controller.core.capture.calibration import ( CalibrationSegment, CalibrationConfig, PixelMapper, create_default_calibration, calibration_from_dict, calibration_to_dict, EDGE_ORDER, EDGE_REVERSE, ) from wled_controller.core.capture.screen_capture import BorderPixels def test_calibration_segment(): """Test calibration segment creation.""" segment = CalibrationSegment( edge="top", led_start=0, led_count=40, reverse=False, ) assert segment.edge == "top" assert segment.led_start == 0 assert segment.led_count == 40 assert segment.reverse is False def test_calibration_config_validation(): """Test calibration configuration validation.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_bottom=40, leds_right=30, leds_top=40, leds_left=40, ) assert config.validate() is True assert config.get_total_leds() == 150 def test_calibration_config_all_zero_leds(): """Test validation fails when all LED counts are zero.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", ) with pytest.raises(ValueError, match="at least one LED"): config.validate() def test_calibration_config_negative_led_count(): """Test validation fails with negative LED counts.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_top=-5, leds_bottom=40, ) with pytest.raises(ValueError, match="non-negative"): config.validate() def test_get_segment_for_edge(): """Test getting segment by edge name.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_bottom=40, leds_right=30, ) bottom_seg = config.get_segment_for_edge("bottom") assert bottom_seg is not None assert bottom_seg.led_count == 40 missing_seg = config.get_segment_for_edge("top") assert missing_seg is None def test_build_segments_basic(): """Test that build_segments produces correct output.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_bottom=10, leds_right=20, leds_top=10, leds_left=20, ) segments = config.build_segments() assert len(segments) == 4 # Clockwise from bottom_left: left(up), top(right), right(down), bottom(left) assert segments[0].edge == "left" assert segments[0].led_start == 0 assert segments[0].led_count == 20 assert segments[0].reverse is True assert segments[1].edge == "top" assert segments[1].led_start == 20 assert segments[1].led_count == 10 assert segments[1].reverse is False assert segments[2].edge == "right" assert segments[2].led_start == 30 assert segments[2].led_count == 20 assert segments[2].reverse is False assert segments[3].edge == "bottom" assert segments[3].led_start == 50 assert segments[3].led_count == 10 assert segments[3].reverse is True def test_build_segments_skips_zero_edges(): """Test that edges with 0 LEDs are skipped.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_right=288, leds_top=358, leds_left=288, ) segments = config.build_segments() assert len(segments) == 3 edges = [s.edge for s in segments] assert "bottom" not in edges @pytest.mark.parametrize("start_position,layout", [ ("bottom_left", "clockwise"), ("bottom_left", "counterclockwise"), ("bottom_right", "clockwise"), ("bottom_right", "counterclockwise"), ("top_left", "clockwise"), ("top_left", "counterclockwise"), ("top_right", "clockwise"), ("top_right", "counterclockwise"), ]) def test_build_segments_all_combinations(start_position, layout): """Test build_segments matches lookup tables for all 8 combinations.""" config = CalibrationConfig( layout=layout, start_position=start_position, leds_top=10, leds_right=10, leds_bottom=10, leds_left=10, ) segments = config.build_segments() assert len(segments) == 4 # Verify edge order matches EDGE_ORDER table expected_order = EDGE_ORDER[(start_position, layout)] actual_order = [s.edge for s in segments] assert actual_order == expected_order # Verify reverse flags match EDGE_REVERSE table expected_reverse = EDGE_REVERSE[(start_position, layout)] for seg in segments: assert seg.reverse == expected_reverse[seg.edge], \ f"Mismatch for {start_position}/{layout}/{seg.edge}: expected reverse={expected_reverse[seg.edge]}" # Verify led_start values are cumulative expected_start = 0 for seg in segments: assert seg.led_start == expected_start expected_start += seg.led_count def test_pixel_mapper_initialization(): """Test pixel mapper initialization.""" config = create_default_calibration(150) mapper = PixelMapper(config, interpolation_mode="average") assert mapper.calibration == config assert mapper.interpolation_mode == "average" def test_pixel_mapper_invalid_mode(): """Test pixel mapper with invalid interpolation mode.""" config = create_default_calibration(150) with pytest.raises(ValueError): PixelMapper(config, interpolation_mode="invalid") def test_pixel_mapper_map_border_to_leds(): """Test mapping border pixels to LED colors.""" config = create_default_calibration(40) # 10 per edge mapper = PixelMapper(config) # Create test border pixels (all red) border_pixels = BorderPixels( top=np.full((10, 100, 3), [255, 0, 0], dtype=np.uint8), right=np.full((100, 10, 3), [0, 255, 0], dtype=np.uint8), bottom=np.full((10, 100, 3), [0, 0, 255], dtype=np.uint8), left=np.full((100, 10, 3), [255, 255, 0], dtype=np.uint8), ) led_colors = mapper.map_border_to_leds(border_pixels) assert len(led_colors) == 40 assert all(isinstance(c, tuple) and len(c) == 3 for c in led_colors) # Verify colors are reasonable (allowing for some rounding) # Bottom LEDs should be mostly blue bottom_seg = config.get_segment_for_edge("bottom") bottom_color = led_colors[bottom_seg.led_start] assert bottom_color[2] > 200 # Blue channel high # Top LEDs should be mostly red top_seg = config.get_segment_for_edge("top") top_color = led_colors[top_seg.led_start] assert top_color[0] > 200 # Red channel high def test_pixel_mapper_test_calibration(): """Test calibration testing pattern.""" config = create_default_calibration(100) mapper = PixelMapper(config) # Test top edge led_colors = mapper.test_calibration("top", (255, 0, 0)) assert len(led_colors) == 100 # Top edge should be lit (red) top_segment = config.get_segment_for_edge("top") top_leds = led_colors[top_segment.led_start:top_segment.led_start + top_segment.led_count] assert all(color == (255, 0, 0) for color in top_leds) # Other LEDs should be off other_leds = led_colors[:top_segment.led_start] assert all(color == (0, 0, 0) for color in other_leds) def test_pixel_mapper_test_calibration_invalid_edge(): """Test calibration testing with invalid edge.""" config = CalibrationConfig( layout="clockwise", start_position="bottom_left", leds_bottom=40, ) mapper = PixelMapper(config) with pytest.raises(ValueError): mapper.test_calibration("top", (255, 0, 0)) # Top not in config def test_create_default_calibration(): """Test creating default calibration.""" config = create_default_calibration(150) assert config.layout == "clockwise" assert config.start_position == "bottom_left" assert len(config.segments) == 4 assert config.get_total_leds() == 150 # Check all edges have LEDs assert config.leds_top > 0 assert config.leds_right > 0 assert config.leds_bottom > 0 assert config.leds_left > 0 # Check all edges are present in derived segments edges = {seg.edge for seg in config.segments} assert edges == {"top", "right", "bottom", "left"} def test_create_default_calibration_small_count(): """Test default calibration with small LED count.""" config = create_default_calibration(4) assert config.get_total_leds() == 4 def test_create_default_calibration_invalid(): """Test default calibration with invalid LED count.""" with pytest.raises(ValueError): create_default_calibration(3) # Too few LEDs def test_calibration_from_dict(): """Test creating calibration from new format dictionary.""" data = { "layout": "clockwise", "start_position": "bottom_left", "offset": 5, "leds_top": 40, "leds_right": 30, "leds_bottom": 40, "leds_left": 30, } config = calibration_from_dict(data) assert config.layout == "clockwise" assert config.start_position == "bottom_left" assert config.offset == 5 assert config.leds_top == 40 assert config.leds_right == 30 assert config.leds_bottom == 40 assert config.leds_left == 30 assert config.get_total_leds() == 140 def test_calibration_from_dict_missing_field(): """Test calibration from dict with missing field.""" data = { "layout": "clockwise", # Missing start_position "leds_top": 10, } with pytest.raises(ValueError): calibration_from_dict(data) def test_calibration_to_dict(): """Test converting calibration to dictionary.""" config = create_default_calibration(100) data = calibration_to_dict(config) assert "layout" in data assert "start_position" in data assert "leds_top" in data assert "leds_right" in data assert "leds_bottom" in data assert "leds_left" in data assert "segments" not in data assert data["leds_top"] + data["leds_right"] + data["leds_bottom"] + data["leds_left"] == 100 def test_calibration_round_trip(): """Test converting calibration to dict and back.""" original = create_default_calibration(120) data = calibration_to_dict(original) restored = calibration_from_dict(data) assert restored.layout == original.layout assert restored.start_position == original.start_position assert restored.leds_top == original.leds_top assert restored.leds_right == original.leds_right assert restored.leds_bottom == original.leds_bottom assert restored.leds_left == original.leds_left assert restored.get_total_leds() == original.get_total_leds() assert len(restored.segments) == len(original.segments)