Files
DOLPHIN/Observability/TUI/test_dolphin_tui_missing_keys.py

284 lines
8.6 KiB
Python
Raw Normal View History

# Tests for graceful "N/A" / "--" rendering when HZ maps return None for all keys.
# Validates: Requirements 12.3
# The TUI MUST NOT crash when any individual HZ key is missing or contains
# malformed JSON. Missing fields MUST render as "--" or "N/A".
import asyncio
import sys
import os
import types
from unittest.mock import MagicMock, patch, AsyncMock
import pytest
# ---------------------------------------------------------------------------
# Ensure the TUI module is importable without textual/hazelcast/httpx installed
# ---------------------------------------------------------------------------
sys.path.insert(0, os.path.dirname(__file__))
for _mod in ("textual", "textual.app", "textual.widgets", "textual.containers", "httpx", "hazelcast"):
if _mod not in sys.modules:
sys.modules[_mod] = types.ModuleType(_mod)
import textual.app as _textual_app
import textual.widgets as _textual_widgets
import textual.containers as _textual_containers
_textual_app.App = object
_textual_app.ComposeResult = object
_textual_widgets.Static = object
_textual_widgets.VerticalScroll = object
_textual_containers.Horizontal = object
from dolphin_tui import (
color_age,
fmt_float,
fmt_pnl,
rm_bar,
posture_color,
status_color,
DataSnapshot,
DolphinDataFetcher,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_mock_map_returning_none():
"""Return a mock IMap whose .get(key).result() always returns None."""
future = MagicMock()
future.result.return_value = None
hz_map = MagicMock()
hz_map.get.return_value = future
hz_map.key_set.return_value = future
return hz_map
def _make_fetcher_with_mock_client():
"""Create a DolphinDataFetcher whose hz_client returns None for every map key."""
fetcher = DolphinDataFetcher()
fetcher.hz_connected = True
mock_map = _make_mock_map_returning_none()
map_future = MagicMock()
map_future.result.return_value = mock_map
mock_client = MagicMock()
mock_client.get_map.return_value = map_future
fetcher.hz_client = mock_client
return fetcher
def _run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
def _fetch_with_empty_maps():
fetcher = _make_fetcher_with_mock_client()
with patch.object(fetcher, "fetch_prefect", new=AsyncMock(return_value=(False, []))):
with patch.object(fetcher, "tail_log", return_value=[]):
with patch.object(fetcher, "_start_reconnect", return_value=None):
return _run(fetcher.fetch())
# ---------------------------------------------------------------------------
# Formatting helpers: None inputs
# ---------------------------------------------------------------------------
def test_fmt_float_none_returns_double_dash():
assert fmt_float(None) == "--"
def test_fmt_float_none_custom_decimals():
assert fmt_float(None, decimals=2) == "--"
def test_fmt_pnl_none_returns_double_dash():
color, text = fmt_pnl(None)
assert text == "--"
def test_fmt_pnl_none_returns_white_color():
color, text = fmt_pnl(None)
assert color == "white"
def test_color_age_none_returns_na():
color, text = color_age(None)
assert text == "N/A"
def test_color_age_none_returns_dim():
color, text = color_age(None)
assert color == "dim"
def test_rm_bar_none_returns_double_dash():
assert rm_bar(None) == "--"
def test_posture_color_none_returns_dim():
assert posture_color(None) == "dim"
def test_status_color_none_returns_dim():
assert status_color(None) == "dim"
# ---------------------------------------------------------------------------
# Sync parsers: None input -> all-None output, no crash
# ---------------------------------------------------------------------------
def test_parse_scan_none_no_crash():
fetcher = DolphinDataFetcher()
result = fetcher._parse_scan(None)
assert result["scan_number"] is None
assert result["vel_div"] is None
assert result["w50_velocity"] is None
assert result["w750_velocity"] is None
assert result["instability_50"] is None
assert result["scan_bridge_ts"] is None
assert result["scan_age_s"] is None
assert result["asset_prices"] == {}
def test_parse_safety_none_no_crash():
fetcher = DolphinDataFetcher()
result = fetcher._parse_safety(None)
for key in ("posture", "rm", "cat1", "cat2", "cat3", "cat4", "cat5"):
assert result[key] is None, "Expected {} to be None".format(key)
def test_parse_heartbeat_none_no_crash():
fetcher = DolphinDataFetcher()
result = fetcher._parse_heartbeat(None)
for key in ("heartbeat_ts", "heartbeat_phase", "heartbeat_flow", "heartbeat_age_s"):
assert result[key] is None, "Expected {} to be None".format(key)
# ---------------------------------------------------------------------------
# fetch() with all-None HZ maps -> DataSnapshot with all HZ fields None
# ---------------------------------------------------------------------------
def test_fetch_empty_maps_returns_datasnapshot():
snap = _fetch_with_empty_maps()
assert isinstance(snap, DataSnapshot)
def test_fetch_empty_maps_hz_connected_true():
snap = _fetch_with_empty_maps()
assert snap.hz_connected is True
def test_fetch_empty_maps_scan_fields_none():
snap = _fetch_with_empty_maps()
assert snap.scan_number is None
assert snap.vel_div is None
assert snap.w50_velocity is None
assert snap.w750_velocity is None
assert snap.instability_50 is None
assert snap.scan_bridge_ts is None
assert snap.scan_age_s is None
def test_fetch_empty_maps_safety_fields_none():
snap = _fetch_with_empty_maps()
assert snap.posture is None
assert snap.rm is None
assert snap.cat1 is None
assert snap.cat2 is None
assert snap.cat3 is None
assert snap.cat4 is None
assert snap.cat5 is None
def test_fetch_empty_maps_extf_fields_none():
snap = _fetch_with_empty_maps()
assert snap.funding_btc is None
assert snap.dvol_btc is None
assert snap.fng is None
assert snap.vix is None
assert snap.exf_age_s is None
def test_fetch_empty_maps_state_fields_none():
snap = _fetch_with_empty_maps()
assert snap.capital is None
assert snap.pnl is None
assert snap.trades is None
assert snap.nautilus_capital is None
assert snap.nautilus_pnl is None
assert snap.nautilus_trades is None
def test_fetch_empty_maps_heartbeat_fields_none():
snap = _fetch_with_empty_maps()
assert snap.heartbeat_ts is None
assert snap.heartbeat_phase is None
assert snap.heartbeat_age_s is None
def test_fetch_empty_maps_meta_health_fields_none():
snap = _fetch_with_empty_maps()
assert snap.meta_rm is None
assert snap.meta_status is None
assert snap.m1_proc is None
assert snap.m2_heartbeat is None
assert snap.m3_data is None
def test_fetch_empty_maps_obf_top_empty_list():
snap = _fetch_with_empty_maps()
assert snap.obf_top == []
# ---------------------------------------------------------------------------
# All-None DataSnapshot: formatting helpers must not crash
# ---------------------------------------------------------------------------
def test_all_none_snap_fmt_float_fields():
snap = DataSnapshot()
for field_name in (
"vel_div", "w50_velocity", "w750_velocity", "instability_50",
"acb_boost", "acb_beta", "funding_btc", "dvol_btc", "fng",
"vix", "capital", "pnl", "rm",
):
val = getattr(snap, field_name)
result = fmt_float(val)
assert result == "--", "fmt_float({}=None) should be '--', got {!r}".format(field_name, result)
def test_all_none_snap_fmt_pnl_fields():
snap = DataSnapshot()
for field_name in ("pnl", "nautilus_pnl"):
val = getattr(snap, field_name)
color, text = fmt_pnl(val)
assert text == "--", "fmt_pnl({}=None) text should be '--'".format(field_name)
assert color == "white"
def test_all_none_snap_color_age_fields():
snap = DataSnapshot()
for field_name in ("scan_age_s", "exf_age_s", "esof_age_s", "heartbeat_age_s"):
val = getattr(snap, field_name)
color, text = color_age(val)
assert text == "N/A", "color_age({}=None) text should be 'N/A'".format(field_name)
assert color == "dim"
def test_all_none_snap_rm_bar():
snap = DataSnapshot()
assert rm_bar(snap.rm) == "--"
def test_all_none_snap_posture_color():
snap = DataSnapshot()
assert posture_color(snap.posture) == "dim"
def test_all_none_snap_status_color():
snap = DataSnapshot()
assert status_color(snap.meta_status) == "dim"