forked from bitcoinafterlife/bal-electrum-plugin
feat(net): invio/ping/download verso Will-Executor in parallelo (anti-freeze)
Problema: i server Will-Executor venivano contattati in sequenza e, su timeout, send_request riprovava 10x con sleep 3s (~130s per server morto). Un solo server irraggiungibile bloccava l'intera operazione (impallamento UI). Soluzione: - ThreadPoolExecutor: ping e push ora in parallelo (tempo ~= server piu' lento, non la somma). Un server morto non blocca piu' gli altri. - Fast-fail per operazioni interattive (ping/info/download): max_retries=0, niente retry-storm. - Feedback live: callback on_each() aggiorna il dialog server-per-server (thread-safe via pyqtSignal di BalWaitingDialog.update). - Push transazioni: parallelo ma con retry per-server mantenuti (no perdita tx). File: - bal/core/willexecutors.py: send_request(+max_retries,+retry_sleep), get_info_task fast-fail, NEW ping_servers_parallel(), push_transactions_parallel(), DEFAULT_TIMEOUT=5. - bal/gui/qt/window.py: ping_willexecutors_task + push_transactions_to_willexecutors riscritti su helper paralleli con feedback live; fetch_will_executors_list fast-fail. - bal/core/util.py: BUGFIX get_value_amount usava in_output (bool) invece di din_output (tupla) -> TypeError. Scoperto dai test ufficiali Gitea. Test (contro il codice refactor): - pytest tests/ ufficiali: 117 core + 65 gui = 182 passed. - smoke/external_zip/windows_overflow/gui_fixes: OK. - parallel_ping_test (nuovo): 0.50s per 8 server vs ~4.00s sequenziale. - ruff: nessun nuovo problema introdotto (codice nuovo PEP8-compliant). Aggiunti i test ufficiali del repo Gitea + REPORT_NETWORKING_PARALLELO.md.
This commit is contained in:
259
tests/test_core_plugin_base.py
Normal file
259
tests/test_core_plugin_base.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Comprehensive tests for ``bal.core.plugin_base``.
|
||||
|
||||
Covers BalTimestamp, BalConfig, and BalPlugin static helpers.
|
||||
|
||||
Run:
|
||||
source electrum/env/bin/activate
|
||||
python3 tests/test_core_plugin_base.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from datetime import datetime, date, timedelta
|
||||
from bal.core.plugin_base import BalTimestamp, BalPlugin, BalConfig
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalTimestamp
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_bt_create_and_str():
|
||||
bt = BalTimestamp("30d")
|
||||
assert bt.unit == "d"
|
||||
assert bt.value == 30
|
||||
|
||||
bt2 = BalTimestamp("1y")
|
||||
assert bt2.unit == "y"
|
||||
assert bt2.value == 1
|
||||
|
||||
bt3 = BalTimestamp(1700000000)
|
||||
assert bt3.unit is None
|
||||
assert bt3.value == 1700000000
|
||||
|
||||
bt4 = BalTimestamp("garbage")
|
||||
# fallback: value=1, unit=None
|
||||
assert bt4.value == 1
|
||||
assert bt4.unit is None
|
||||
|
||||
bt5 = BalTimestamp(0)
|
||||
assert bt5.unit is None
|
||||
assert bt5.value == 0
|
||||
|
||||
bt6 = BalTimestamp("7d")
|
||||
assert str(bt6) == "7d"
|
||||
|
||||
bt7 = BalTimestamp("2y")
|
||||
assert str(bt7) == "2y"
|
||||
|
||||
# absolute timestamp str -> ISO format
|
||||
bt8 = BalTimestamp(1700000000)
|
||||
s = str(bt8)
|
||||
assert "202" in s or "197" in s # year present
|
||||
|
||||
|
||||
def test_bt_duration_to_days():
|
||||
assert BalTimestamp("30d").duration_to_days() == 30
|
||||
assert BalTimestamp("1y").duration_to_days() == 365
|
||||
assert BalTimestamp("0d").duration_to_days() == 0
|
||||
assert BalTimestamp(1700000000).duration_to_days() == 1700000000 # unit None -> raw value
|
||||
|
||||
|
||||
def test_bt_to_date_absolute():
|
||||
bt = BalTimestamp(1700000000)
|
||||
d = bt.to_date()
|
||||
assert isinstance(d, datetime)
|
||||
|
||||
# absolute with from_date (should be ignored for absolute)
|
||||
d2 = bt.to_date(from_date=datetime(2020, 1, 1))
|
||||
assert d == d2
|
||||
|
||||
|
||||
def test_bt_to_date_relative():
|
||||
now = datetime.now()
|
||||
|
||||
# relative days from now
|
||||
bt = BalTimestamp("7d")
|
||||
d = bt.to_date()
|
||||
assert d.hour == 0 and d.minute == 0 # normalized to midnight
|
||||
assert d > now
|
||||
|
||||
# reverse (subtract days)
|
||||
d_rev = bt.to_date(reverse=True)
|
||||
assert d_rev < now
|
||||
|
||||
# from explicit datetime
|
||||
base = datetime(2025, 6, 1, 12, 0, 0)
|
||||
d = bt.to_date(from_date=base)
|
||||
expected = (base + timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
assert d == expected
|
||||
|
||||
# from int timestamp
|
||||
ts = int(base.timestamp())
|
||||
d = bt.to_date(from_date=ts)
|
||||
assert d == expected
|
||||
|
||||
|
||||
def test_bt_to_date_years():
|
||||
bt = BalTimestamp("1y")
|
||||
d = bt.to_date()
|
||||
assert d > datetime.now()
|
||||
|
||||
|
||||
def test_bt_to_date_overflow():
|
||||
"""Huge relative durations should not crash (clamp to INT32_MAX)."""
|
||||
bt = BalTimestamp("999999999d")
|
||||
d = bt.to_date()
|
||||
# should not raise
|
||||
assert d is not None
|
||||
assert isinstance(d, datetime)
|
||||
|
||||
|
||||
def test_bt_to_timestamp():
|
||||
bt = BalTimestamp("7d")
|
||||
ts = bt.to_timestamp()
|
||||
assert ts > time.time()
|
||||
assert isinstance(ts, float)
|
||||
|
||||
bt2 = BalTimestamp(1700000000)
|
||||
assert abs(bt2.to_timestamp() - 1700000000) < 86400 # close to original
|
||||
|
||||
|
||||
def test_bt_repr():
|
||||
assert repr(BalTimestamp("7d")) == "7d"
|
||||
r = repr(BalTimestamp(1700000000))
|
||||
assert isinstance(r, str)
|
||||
assert len(r) > 0
|
||||
|
||||
|
||||
def test_bt_edge_values():
|
||||
# zero timestamp
|
||||
bt0 = BalTimestamp(0)
|
||||
d = bt0.to_date()
|
||||
assert d is not None
|
||||
|
||||
# negative? (may depend on platform)
|
||||
try:
|
||||
bt_neg = BalTimestamp(-1)
|
||||
_ = bt_neg.to_date()
|
||||
except (OSError, ValueError, OverflowError):
|
||||
pass # acceptable on some platforms
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalTimestamp._safe_fromtimestamp
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_safe_fromtimestamp_normal():
|
||||
d = BalTimestamp._safe_fromtimestamp(1700000000)
|
||||
assert isinstance(d, datetime)
|
||||
|
||||
|
||||
def test_safe_fromtimestamp_nlocktime_max():
|
||||
"""NLOCKTIME_MAX (2**32-1) must not raise even on 32-bit platforms."""
|
||||
d = BalTimestamp._safe_fromtimestamp(2**32 - 1)
|
||||
assert d is not None
|
||||
|
||||
|
||||
def test_safe_fromtimestamp_negative():
|
||||
"""Negative timestamps should not crash."""
|
||||
d = BalTimestamp._safe_fromtimestamp(-1)
|
||||
assert isinstance(d, datetime)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalConfig
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
class FakeConfig:
|
||||
"""Minimal mock for Electrum config."""
|
||||
def __init__(self):
|
||||
self._store = {}
|
||||
def get(self, key, default=None):
|
||||
return self._store.get(key, default)
|
||||
def set_key(self, key, value, save=True):
|
||||
self._store[key] = value
|
||||
|
||||
|
||||
def test_balconfig_default():
|
||||
cfg = FakeConfig()
|
||||
bc = BalConfig(cfg, "test_key", "default_val")
|
||||
assert bc.get() == "default_val"
|
||||
assert bc.get("override") == "override"
|
||||
assert bc.get(None) == "default_val"
|
||||
|
||||
|
||||
def test_balconfig_set():
|
||||
cfg = FakeConfig()
|
||||
bc = BalConfig(cfg, "test_key", "default_val")
|
||||
bc.set("stored_val")
|
||||
assert cfg.get("test_key") == "stored_val"
|
||||
assert bc.get() == "stored_val"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalPlugin
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_default_will_settings_relative():
|
||||
rel = BalPlugin.default_will_settings_relative()
|
||||
assert rel["threshold"] == "30d"
|
||||
assert rel["locktime"] == "1y"
|
||||
|
||||
|
||||
def test_default_will_settings():
|
||||
settings = BalPlugin.default_will_settings()
|
||||
assert settings["baltx_fees"] == 100
|
||||
assert "threshold" in settings
|
||||
assert "locktime" in settings
|
||||
# threshold/locktime should be absolute timestamps
|
||||
assert isinstance(settings["threshold"], float)
|
||||
assert isinstance(settings["locktime"], float)
|
||||
|
||||
|
||||
def test_default_will_settings_absolute():
|
||||
abs_ = BalPlugin.default_will_settings_absolute()
|
||||
assert "threshold" in abs_
|
||||
assert "locktime" in abs_
|
||||
# should be timestamps (in the future)
|
||||
today = datetime.combine(date.today(), datetime.min.time())
|
||||
assert abs_["threshold"] >= today.timestamp()
|
||||
assert abs_["locktime"] >= today.timestamp()
|
||||
|
||||
|
||||
def test_validate_will_settings():
|
||||
# Note: passing None triggers `will_settings = []` which then fails
|
||||
# on .get(). This is a latent bug — test passing a dict directly
|
||||
result = BalPlugin.validate_will_settings(None, {"baltx_fees": 0})
|
||||
assert result["baltx_fees"] == 100
|
||||
|
||||
# normal settings unchanged
|
||||
input_settings = {"baltx_fees": 50, "threshold": 1700000000, "locktime": 1800000000}
|
||||
result = BalPlugin.validate_will_settings(None, input_settings)
|
||||
assert result["baltx_fees"] == 50
|
||||
assert result["threshold"] == 1700000000
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_bt_create_and_str()
|
||||
test_bt_duration_to_days()
|
||||
test_bt_to_date_absolute()
|
||||
test_bt_to_date_relative()
|
||||
test_bt_to_date_years()
|
||||
test_bt_to_date_overflow()
|
||||
test_bt_to_timestamp()
|
||||
test_bt_repr()
|
||||
test_bt_edge_values()
|
||||
test_safe_fromtimestamp_normal()
|
||||
test_safe_fromtimestamp_nlocktime_max()
|
||||
test_safe_fromtimestamp_negative()
|
||||
test_balconfig_default()
|
||||
test_balconfig_set()
|
||||
test_default_will_settings_relative()
|
||||
test_default_will_settings()
|
||||
test_default_will_settings_absolute()
|
||||
test_validate_will_settings()
|
||||
print(f"[OK] All {sum(1 for k in dir() if k.startswith('test_'))} plugin_base tests passed")
|
||||
Reference in New Issue
Block a user