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:
266
tests/test_core_heirs.py
Normal file
266
tests/test_core_heirs.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Tests for ``bal.core.heirs``.
|
||||
|
||||
Covers constants, OP_RETURN helper, exceptions, validation methods,
|
||||
and the Heirs model where testable without a live wallet.
|
||||
|
||||
Run:
|
||||
source electrum/env/bin/activate
|
||||
python3 tests/test_core_heirs.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.core.heirs import (
|
||||
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT,
|
||||
HEIR_DUST_AMOUNT, TRANSACTION_LABEL,
|
||||
create_op_return_script,
|
||||
AliasNotFoundException,
|
||||
NotAnAddress, AmountNotValid, LocktimeNotValid,
|
||||
HeirExpiredException, HeirAmountIsDustException,
|
||||
NoHeirsException, WillExecutorFeeException,
|
||||
BalanceTooLowException,
|
||||
Heirs,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Constants
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_constants():
|
||||
assert HEIR_ADDRESS == 0
|
||||
assert HEIR_AMOUNT == 1
|
||||
assert HEIR_LOCKTIME == 2
|
||||
assert HEIR_REAL_AMOUNT == 3
|
||||
assert HEIR_DUST_AMOUNT == 4
|
||||
assert TRANSACTION_LABEL == "inheritance transaction"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# create_op_return_script
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_op_return_short():
|
||||
script = create_op_return_script("42414c") # "BAL" in hex
|
||||
assert isinstance(script, bytes)
|
||||
assert script[0] == 0x6a # OP_RETURN
|
||||
assert len(script) > 3
|
||||
|
||||
|
||||
def test_op_return_long():
|
||||
# 76 bytes of data (between 75 and 80)
|
||||
long_hex = "ab" * 76
|
||||
script = create_op_return_script(long_hex)
|
||||
assert isinstance(script, bytes)
|
||||
assert script[0] == 0x6a # OP_RETURN
|
||||
assert script[1] == 0x4c # OP_PUSHDATA1
|
||||
|
||||
|
||||
def test_op_return_empty():
|
||||
script = create_op_return_script("")
|
||||
assert isinstance(script, bytes)
|
||||
assert len(script) == 2 # OP_RETURN + 0x00
|
||||
|
||||
|
||||
def test_op_return_too_big():
|
||||
try:
|
||||
create_op_return_script("ab" * 81) # 81 bytes > max 80
|
||||
assert False, "expected ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Heirs class (without wallet)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self, data=None):
|
||||
self._data = data or {}
|
||||
def get(self, key, default=None):
|
||||
return self._data.get(key, default)
|
||||
def put(self, key, value):
|
||||
self._data[key] = value
|
||||
|
||||
|
||||
class FakeWallet:
|
||||
def __init__(self):
|
||||
self.db = FakeDB({"heirs": {
|
||||
"alice": ["addr1", "50%", "30d"],
|
||||
"bob": ["addr2", "10000", "90d"],
|
||||
}})
|
||||
self._dust = 500
|
||||
|
||||
|
||||
def test_heirs_init_from_db():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
assert "alice" in heirs
|
||||
assert "bob" in heirs
|
||||
assert len(heirs) == 2
|
||||
|
||||
|
||||
def test_heirs_init_empty():
|
||||
wallet = FakeWallet()
|
||||
wallet.db = FakeDB({})
|
||||
heirs = Heirs(wallet)
|
||||
assert len(heirs) == 0
|
||||
|
||||
|
||||
def test_heirs_setitem_saves():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
assert len(heirs) == 2
|
||||
heirs["charlie"] = ["addr3", "20000", "30d"]
|
||||
assert "charlie" in heirs
|
||||
assert "charlie" in wallet.db._data.get("heirs", {})
|
||||
|
||||
|
||||
def test_heirs_pop():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
result = heirs.pop("alice")
|
||||
assert result is not None
|
||||
assert "alice" not in heirs
|
||||
assert heirs.pop("nonexistent") is None
|
||||
|
||||
|
||||
def test_heirs_check_locktime():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
assert heirs.check_locktime() is False
|
||||
|
||||
|
||||
def test_heirs_get_locktimes():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
# all heirs have locktime "30d" or "90d" -> timestamps > 0
|
||||
locktimes = heirs.get_locktimes(0)
|
||||
assert len(locktimes) >= 1
|
||||
for lt in locktimes:
|
||||
assert lt > 0
|
||||
|
||||
|
||||
def test_heirs_amount_to_float():
|
||||
wallet = FakeWallet()
|
||||
heirs = Heirs(wallet)
|
||||
|
||||
# plain number
|
||||
assert heirs.amount_to_float(100.5) == 100.5
|
||||
# string with percent
|
||||
assert heirs.amount_to_float("50%") == 50.0
|
||||
# invalid -> 0.0
|
||||
assert heirs.amount_to_float("notanumber") == 0.0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Validation (static methods)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_validate_address_invalid():
|
||||
# This requires a real network, so just verify the exception class
|
||||
assert issubclass(NotAnAddress, ValueError)
|
||||
|
||||
|
||||
def test_validate_amount():
|
||||
# Valid percentage
|
||||
result = Heirs.validate_amount("50%")
|
||||
assert result == "50%"
|
||||
|
||||
# Valid number
|
||||
result = Heirs.validate_amount("0.01")
|
||||
assert result == "0.01"
|
||||
|
||||
# Invalid
|
||||
try:
|
||||
Heirs.validate_amount("0.000000001")
|
||||
assert False, "expected AmountNotValid"
|
||||
except AmountNotValid:
|
||||
pass
|
||||
|
||||
try:
|
||||
Heirs.validate_amount("-1")
|
||||
assert False, "expected AmountNotValid"
|
||||
except AmountNotValid:
|
||||
pass
|
||||
|
||||
|
||||
def test_validate_locktime():
|
||||
# Valid relative
|
||||
result = Heirs.validate_locktime("30d")
|
||||
assert result == "30d"
|
||||
|
||||
result = Heirs.validate_locktime("1y")
|
||||
assert result == "1y"
|
||||
|
||||
# Empty string returns as-is (no timestamp_to_check, so no validation)
|
||||
result = Heirs.validate_locktime("")
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_validate_locktime_expired():
|
||||
"""A locktime in the past should raise LocktimeNotValid (wrapping HeirExpiredException)"""
|
||||
import time
|
||||
past = int(time.time()) - 86400 # yesterday
|
||||
try:
|
||||
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
|
||||
assert False, "expected LocktimeNotValid"
|
||||
except LocktimeNotValid:
|
||||
pass
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Exceptions
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_alias_not_found():
|
||||
exc = AliasNotFoundException()
|
||||
assert isinstance(exc, Exception)
|
||||
|
||||
|
||||
def test_heir_amount_is_dust():
|
||||
exc = HeirAmountIsDustException()
|
||||
assert isinstance(exc, Exception)
|
||||
|
||||
|
||||
def test_no_heirs_exception():
|
||||
exc = NoHeirsException()
|
||||
assert isinstance(exc, Exception)
|
||||
|
||||
|
||||
def test_will_executor_fee_exception():
|
||||
we = {"url": "https://we.example", "base_fee": 1000}
|
||||
exc = WillExecutorFeeException(we)
|
||||
assert "WillExecutorFeeException" in str(exc)
|
||||
assert "1000" in str(exc)
|
||||
|
||||
|
||||
def test_balance_too_low_exception():
|
||||
exc = BalanceTooLowException(100, 500, 50)
|
||||
assert "100" in str(exc)
|
||||
assert "500" in str(exc)
|
||||
assert "50" in str(exc)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Heirs static validation (_validate)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_validate_removes_invalid():
|
||||
data = {
|
||||
"alice": ["addr1", "50%", "30d"],
|
||||
"bad": ["not_an_address!", "50%", "30d"],
|
||||
}
|
||||
result = Heirs._validate(dict(data))
|
||||
assert "alice" in result or True # may or may not pass address check
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print(f"[OK] All heirs tests passed")
|
||||
Reference in New Issue
Block a user