"""Tests for the radar scope widget — LUT geometry and pixel intensity.""" from skywalker_tui.widgets.radar_scope import RadarScope class TestRadarGeometry: """LUT pre-computation and geometry tests.""" def _make_scope(self, width=40, height=20): scope = RadarScope() scope._recompute_geometry(width, height) return scope def test_lut_dimensions(self): scope = self._make_scope(40, 20) # pixel rows = terminal rows * 2 assert len(scope._dist_lut) == 40 assert len(scope._angle_lut) == 40 assert len(scope._dx_lut) == 40 assert len(scope._dy_lut) == 40 # each row has `width` columns assert len(scope._dist_lut[0]) == 40 assert len(scope._angle_lut[0]) == 40 def test_center_pixel_distance_near_zero(self): scope = self._make_scope(40, 20) # center = (20, 20) in pixel coords cx_int = int(scope._cx) cy_int = int(scope._cy) dist = scope._dist_lut[cy_int][cx_int] assert dist < 1.0, f"Center pixel distance should be ~0, got {dist}" def test_corner_pixel_outside_circle(self): scope = self._make_scope(40, 20) dist = scope._dist_lut[0][0] assert dist > scope._radius, "Corner should be outside the radar circle" def test_radius_fits_within_bounds(self): scope = self._make_scope(60, 30) # radius should be ≤ half the smaller dimension assert scope._radius <= 30 # half of width assert scope._radius <= 29 # half of pixel height (60) - 1 def test_recompute_updates_on_resize(self): scope = self._make_scope(40, 20) r1 = scope._radius scope._recompute_geometry(80, 40) r2 = scope._radius assert r2 > r1, "Larger terminal should give larger radius" class TestPixelIntensity: """Tests for _pixel_intensity with pre-computed LUT values.""" def _make_scope(self, width=40, height=20): scope = RadarScope() scope._recompute_geometry(width, height) return scope def test_outside_circle_returns_zero(self): scope = self._make_scope() samples = [0.0] * 360 result = scope._pixel_intensity( dist=scope._radius + 5, sample_idx=0, dx=20.0, dy=0.0, radius=scope._radius, samples=samples, angle_idx=0, locked=False, ) assert result == 0 def test_center_on_crosshair(self): """At exact center, crosshair check (abs(dx) < 0.7) fires before center dot.""" scope = self._make_scope() samples = [0.0] * 360 result = scope._pixel_intensity( dist=0.5, sample_idx=0, dx=0.0, dy=0.0, radius=scope._radius, samples=samples, angle_idx=0, locked=False, ) assert result == 2 # crosshair overlaps center def test_center_off_crosshair(self): """Near center but off crosshair axes → center dot (1).""" scope = self._make_scope() samples = [0.0] * 360 result = scope._pixel_intensity( dist=0.8, sample_idx=45, dx=0.8, dy=0.8, radius=scope._radius, samples=samples, angle_idx=0, locked=False, ) assert result == 1 # center dot, off crosshair axes def test_lock_ring_at_edge(self): scope = self._make_scope() samples = [0.0] * 360 result = scope._pixel_intensity( dist=scope._radius, sample_idx=0, dx=scope._radius, dy=0.0, radius=scope._radius, samples=samples, angle_idx=0, locked=True, ) assert result == 7 # peak phosphor for lock ring def test_no_lock_ring_when_unlocked(self): scope = self._make_scope() samples = [0.0] * 360 result = scope._pixel_intensity( dist=scope._radius, sample_idx=0, dx=scope._radius, dy=0.0, radius=scope._radius, samples=samples, angle_idx=0, locked=False, ) # Should be boundary ring (2), not lock ring (7) assert result != 7 def test_crosshair_on_vertical(self): scope = self._make_scope() samples = [0.0] * 360 # Point on the vertical crosshair (dx near 0, inside circle) result = scope._pixel_intensity( dist=scope._radius * 0.5, sample_idx=90, dx=0.0, dy=scope._radius * 0.5, radius=scope._radius, samples=samples, angle_idx=0, locked=False, ) assert result == 2 # crosshair intensity def test_strong_signal_blip_near_sweep(self): scope = self._make_scope() samples = [0.0] * 360 # strength 0.47 → blip at 0.47 * r * 0.85 = 0.40*r # Clear of range rings at 0.25r, 0.50r, 0.75r (nearest gap: 0.15r ≈ 2.85px) samples[180] = 0.47 signal_dist = 0.47 * scope._radius * 0.85 # Verify we're not on a range ring r = scope._radius for ring_r in (0.25, 0.5, 0.75): assert abs(signal_dist - r * ring_r) > 0.7, ( f"Signal blip at {signal_dist:.2f} overlaps range ring at {r * ring_r:.2f}" ) result = scope._pixel_intensity( dist=signal_dist, sample_idx=180, dx=5.0, dy=5.0, radius=scope._radius, samples=samples, angle_idx=180, locked=False, ) # Newest sample at sweep position should be visible assert result >= 3 def test_old_signal_decays(self): scope = self._make_scope() samples = [0.0] * 360 samples[0] = 0.8 signal_dist = 0.8 * scope._radius * 0.85 # Sample at index 0, sweep beam at index 300 → age = 300 result = scope._pixel_intensity( dist=signal_dist, sample_idx=0, dx=5.0, dy=5.0, radius=scope._radius, samples=samples, angle_idx=300, locked=False, ) # Old sample should be dim or invisible assert result <= 2 class TestRadarPush: """Tests for the push() method and normalization.""" def test_push_normalizes_to_range(self): scope = RadarScope(max_samples=10) scope.push(8.0, max_snr=16.0) assert scope._samples[-1] == 0.5 def test_push_clamps_above_max(self): scope = RadarScope(max_samples=10) scope.push(20.0, max_snr=16.0) assert scope._samples[-1] == 1.0 def test_push_clamps_below_zero(self): scope = RadarScope(max_samples=10) scope.push(-5.0, max_snr=16.0) assert scope._samples[-1] == 0.0 def test_angle_index_wraps(self): scope = RadarScope(max_samples=4) for _ in range(5): scope.push(1.0) assert scope._angle_idx == 1 # 5 % 4 = 1 def test_set_locked(self): scope = RadarScope() assert scope._locked is False scope.set_locked(True) assert scope._locked is True