Normalize line endings to LF across entire repository
Apply .gitattributes normalization to convert all CRLF line endings inherited from Windows-origin source files to Unix LF. 175 files, zero content changes.
This commit is contained in:
parent
696d2dd387
commit
bbdcb243dc
175 changed files with 56794 additions and 56794 deletions
|
|
@ -1,192 +1,192 @@
|
|||
"""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
|
||||
"""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
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
"""Tests for the splash screen art catalog and selection."""
|
||||
|
||||
from skywalker_tui.screens.splash import SplashScreen, ASSETS_DIR, ART_CATALOG
|
||||
|
||||
|
||||
class TestSplashArtCatalog:
|
||||
"""Verify bundled pre-baked .ans art assets exist and catalog is consistent."""
|
||||
|
||||
def test_assets_dir_exists(self):
|
||||
assert ASSETS_DIR.is_dir(), f"Assets dir missing: {ASSETS_DIR}"
|
||||
|
||||
def test_all_catalog_entries_have_ans_files(self):
|
||||
missing = []
|
||||
for stem, artist, title in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if not path.exists():
|
||||
missing.append(f"{stem}.ans")
|
||||
assert not missing, f"Missing pre-baked .ans files: {missing}"
|
||||
|
||||
def test_catalog_has_entries(self):
|
||||
assert len(ART_CATALOG) >= 1
|
||||
|
||||
def test_ans_files_not_empty(self):
|
||||
for stem, _, _ in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if path.exists():
|
||||
assert path.stat().st_size > 0, f"{stem}.ans is empty"
|
||||
|
||||
def test_ans_files_contain_ansi_escapes(self):
|
||||
"""Pre-baked files should contain ANSI color escape sequences."""
|
||||
for stem, _, _ in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if path.exists():
|
||||
content = path.read_text()
|
||||
assert "\033[" in content, f"{stem}.ans has no ANSI escapes"
|
||||
assert "\u2580" in content, f"{stem}.ans has no half-block chars"
|
||||
|
||||
|
||||
class TestSplashScreenInit:
|
||||
"""Test SplashScreen construction (no app needed)."""
|
||||
|
||||
def test_selects_art_on_init(self):
|
||||
screen = SplashScreen()
|
||||
assert screen._ans_path is not None or len(ART_CATALOG) == 0
|
||||
if screen._ans_path:
|
||||
assert screen._ans_path.exists()
|
||||
assert screen._artist != ""
|
||||
assert screen._title != ""
|
||||
"""Tests for the splash screen art catalog and selection."""
|
||||
|
||||
from skywalker_tui.screens.splash import SplashScreen, ASSETS_DIR, ART_CATALOG
|
||||
|
||||
|
||||
class TestSplashArtCatalog:
|
||||
"""Verify bundled pre-baked .ans art assets exist and catalog is consistent."""
|
||||
|
||||
def test_assets_dir_exists(self):
|
||||
assert ASSETS_DIR.is_dir(), f"Assets dir missing: {ASSETS_DIR}"
|
||||
|
||||
def test_all_catalog_entries_have_ans_files(self):
|
||||
missing = []
|
||||
for stem, artist, title in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if not path.exists():
|
||||
missing.append(f"{stem}.ans")
|
||||
assert not missing, f"Missing pre-baked .ans files: {missing}"
|
||||
|
||||
def test_catalog_has_entries(self):
|
||||
assert len(ART_CATALOG) >= 1
|
||||
|
||||
def test_ans_files_not_empty(self):
|
||||
for stem, _, _ in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if path.exists():
|
||||
assert path.stat().st_size > 0, f"{stem}.ans is empty"
|
||||
|
||||
def test_ans_files_contain_ansi_escapes(self):
|
||||
"""Pre-baked files should contain ANSI color escape sequences."""
|
||||
for stem, _, _ in ART_CATALOG:
|
||||
path = ASSETS_DIR / f"{stem}.ans"
|
||||
if path.exists():
|
||||
content = path.read_text()
|
||||
assert "\033[" in content, f"{stem}.ans has no ANSI escapes"
|
||||
assert "\u2580" in content, f"{stem}.ans has no half-block chars"
|
||||
|
||||
|
||||
class TestSplashScreenInit:
|
||||
"""Test SplashScreen construction (no app needed)."""
|
||||
|
||||
def test_selects_art_on_init(self):
|
||||
screen = SplashScreen()
|
||||
assert screen._ans_path is not None or len(ART_CATALOG) == 0
|
||||
if screen._ans_path:
|
||||
assert screen._ans_path.exists()
|
||||
assert screen._artist != ""
|
||||
assert screen._title != ""
|
||||
|
|
|
|||
|
|
@ -1,108 +1,108 @@
|
|||
"""Tests for the stateful telnet IAC sequence stripper."""
|
||||
|
||||
from skywalker_tui.screens.starwars import _TelnetStripper
|
||||
|
||||
|
||||
class TestTelnetStripper:
|
||||
"""Edge cases for IAC parsing across chunk boundaries."""
|
||||
|
||||
def test_plain_text_passthrough(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"hello world") == b"hello world"
|
||||
|
||||
def test_strips_will_command(self):
|
||||
# IAC WILL ECHO = FF FB 01
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfb\x01hello") == b"hello"
|
||||
|
||||
def test_strips_wont_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfc\x03data") == b"data"
|
||||
|
||||
def test_strips_do_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfd\x01data") == b"data"
|
||||
|
||||
def test_strips_dont_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfe\x01data") == b"data"
|
||||
|
||||
def test_escaped_0xff(self):
|
||||
"""Doubled 0xFF = literal 0xFF byte in content."""
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xff") == b"\xff"
|
||||
|
||||
def test_sub_negotiation(self):
|
||||
# IAC SB 0x01 ... IAC SE = FF FA 01 xx xx FF F0
|
||||
s = _TelnetStripper()
|
||||
data = b"before\xff\xfa\x01\x00\x00\xff\xf0after"
|
||||
assert s.feed(data) == b"beforeafter"
|
||||
|
||||
def test_split_iac_across_chunks(self):
|
||||
"""IAC command split: FF in chunk 1, FB 01 in chunk 2."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"hello\xff")
|
||||
out2 = s.feed(b"\xfb\x01world")
|
||||
assert out1 == b"hello"
|
||||
assert out2 == b"world"
|
||||
|
||||
def test_split_will_at_boundary(self):
|
||||
"""IAC WILL split: FF FB in chunk 1, option byte in chunk 2."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"aaa\xff\xfb")
|
||||
out2 = s.feed(b"\x03bbb")
|
||||
assert out1 == b"aaa"
|
||||
assert out2 == b"bbb"
|
||||
|
||||
def test_split_sub_negotiation(self):
|
||||
"""Sub-negotiation without closing IAC SE — buffers until next chunk."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"x\xff\xfa\x01\x00")
|
||||
out2 = s.feed(b"\xff\xf0y")
|
||||
assert out1 == b"x"
|
||||
assert out2 == b"y"
|
||||
|
||||
def test_multiple_commands_in_one_chunk(self):
|
||||
s = _TelnetStripper()
|
||||
data = b"\xff\xfb\x01\xff\xfc\x03text\xff\xfd\x01"
|
||||
assert s.feed(data) == b"text"
|
||||
|
||||
def test_empty_input(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"") == b""
|
||||
|
||||
def test_just_iac_byte(self):
|
||||
"""Single 0xFF byte — should buffer, waiting for next byte."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"\xff")
|
||||
assert out1 == b""
|
||||
out2 = s.feed(b"\xfb\x01done")
|
||||
assert out2 == b"done"
|
||||
|
||||
def test_unknown_iac_command(self):
|
||||
"""Unknown command byte after IAC — stripped as 2-byte sequence."""
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xf1text") == b"text"
|
||||
|
||||
|
||||
class TestFrameParsing:
|
||||
"""Tests for the ESC[H frame boundary detection logic."""
|
||||
|
||||
def test_frame_split_on_cursor_home(self):
|
||||
"""ESC[H splits data into frames."""
|
||||
data = b"frame1\x1b[Hframe2\x1b[Hframe3"
|
||||
frames = data.split(b"\x1b[H")
|
||||
assert frames == [b"frame1", b"frame2", b"frame3"]
|
||||
|
||||
def test_esc_j_in_frame(self):
|
||||
"""ESC[J (clear to end) can be stripped from frame data."""
|
||||
data = b"\x1b[Jsome text here"
|
||||
clean = data.replace(b"\x1b[J", b"")
|
||||
assert clean == b"some text here"
|
||||
|
||||
def test_empty_frames_between_homes(self):
|
||||
"""Consecutive ESC[H produces empty frames (filtered in code)."""
|
||||
data = b"\x1b[H\x1b[Htext"
|
||||
frames = data.split(b"\x1b[H")
|
||||
non_empty = [f for f in frames if f.strip()]
|
||||
assert non_empty == [b"text"]
|
||||
"""Tests for the stateful telnet IAC sequence stripper."""
|
||||
|
||||
from skywalker_tui.screens.starwars import _TelnetStripper
|
||||
|
||||
|
||||
class TestTelnetStripper:
|
||||
"""Edge cases for IAC parsing across chunk boundaries."""
|
||||
|
||||
def test_plain_text_passthrough(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"hello world") == b"hello world"
|
||||
|
||||
def test_strips_will_command(self):
|
||||
# IAC WILL ECHO = FF FB 01
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfb\x01hello") == b"hello"
|
||||
|
||||
def test_strips_wont_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfc\x03data") == b"data"
|
||||
|
||||
def test_strips_do_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfd\x01data") == b"data"
|
||||
|
||||
def test_strips_dont_command(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xfe\x01data") == b"data"
|
||||
|
||||
def test_escaped_0xff(self):
|
||||
"""Doubled 0xFF = literal 0xFF byte in content."""
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xff") == b"\xff"
|
||||
|
||||
def test_sub_negotiation(self):
|
||||
# IAC SB 0x01 ... IAC SE = FF FA 01 xx xx FF F0
|
||||
s = _TelnetStripper()
|
||||
data = b"before\xff\xfa\x01\x00\x00\xff\xf0after"
|
||||
assert s.feed(data) == b"beforeafter"
|
||||
|
||||
def test_split_iac_across_chunks(self):
|
||||
"""IAC command split: FF in chunk 1, FB 01 in chunk 2."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"hello\xff")
|
||||
out2 = s.feed(b"\xfb\x01world")
|
||||
assert out1 == b"hello"
|
||||
assert out2 == b"world"
|
||||
|
||||
def test_split_will_at_boundary(self):
|
||||
"""IAC WILL split: FF FB in chunk 1, option byte in chunk 2."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"aaa\xff\xfb")
|
||||
out2 = s.feed(b"\x03bbb")
|
||||
assert out1 == b"aaa"
|
||||
assert out2 == b"bbb"
|
||||
|
||||
def test_split_sub_negotiation(self):
|
||||
"""Sub-negotiation without closing IAC SE — buffers until next chunk."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"x\xff\xfa\x01\x00")
|
||||
out2 = s.feed(b"\xff\xf0y")
|
||||
assert out1 == b"x"
|
||||
assert out2 == b"y"
|
||||
|
||||
def test_multiple_commands_in_one_chunk(self):
|
||||
s = _TelnetStripper()
|
||||
data = b"\xff\xfb\x01\xff\xfc\x03text\xff\xfd\x01"
|
||||
assert s.feed(data) == b"text"
|
||||
|
||||
def test_empty_input(self):
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"") == b""
|
||||
|
||||
def test_just_iac_byte(self):
|
||||
"""Single 0xFF byte — should buffer, waiting for next byte."""
|
||||
s = _TelnetStripper()
|
||||
out1 = s.feed(b"\xff")
|
||||
assert out1 == b""
|
||||
out2 = s.feed(b"\xfb\x01done")
|
||||
assert out2 == b"done"
|
||||
|
||||
def test_unknown_iac_command(self):
|
||||
"""Unknown command byte after IAC — stripped as 2-byte sequence."""
|
||||
s = _TelnetStripper()
|
||||
assert s.feed(b"\xff\xf1text") == b"text"
|
||||
|
||||
|
||||
class TestFrameParsing:
|
||||
"""Tests for the ESC[H frame boundary detection logic."""
|
||||
|
||||
def test_frame_split_on_cursor_home(self):
|
||||
"""ESC[H splits data into frames."""
|
||||
data = b"frame1\x1b[Hframe2\x1b[Hframe3"
|
||||
frames = data.split(b"\x1b[H")
|
||||
assert frames == [b"frame1", b"frame2", b"frame3"]
|
||||
|
||||
def test_esc_j_in_frame(self):
|
||||
"""ESC[J (clear to end) can be stripped from frame data."""
|
||||
data = b"\x1b[Jsome text here"
|
||||
clean = data.replace(b"\x1b[J", b"")
|
||||
assert clean == b"some text here"
|
||||
|
||||
def test_empty_frames_between_homes(self):
|
||||
"""Consecutive ESC[H produces empty frames (filtered in code)."""
|
||||
data = b"\x1b[H\x1b[Htext"
|
||||
frames = data.split(b"\x1b[H")
|
||||
non_empty = [f for f in frames if f.strip()]
|
||||
assert non_empty == [b"text"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue