284 lines
8.6 KiB
Python
284 lines
8.6 KiB
Python
|
|
# 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"
|