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:
255
tests/test_gui_widgets.py
Normal file
255
tests/test_gui_widgets.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Tests for ``bal.gui.qt.widgets``.
|
||||
|
||||
Covers testable widgets without requiring a running Electrum wallet:
|
||||
- ClickableLabel
|
||||
- BalLineEdit, BalTextEdit, BalCheckBox
|
||||
- _LockTimeEditor (static/class methods)
|
||||
- LockTimeRawEdit (numbify, checkbdy, replace_str)
|
||||
- PercAmountEdit
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_widgets.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from PyQt6.QtCore import QTimer
|
||||
from PyQt6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from electrum.util import DECIMAL_POINT, decimal_point_to_base_unit_name
|
||||
|
||||
_app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# ClickableLabel
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_clickable_label_creation():
|
||||
from bal.gui.qt.widgets import ClickableLabel
|
||||
lbl = ClickableLabel("test")
|
||||
assert lbl.text() == "test"
|
||||
assert hasattr(lbl, "doubleClicked")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalLineEdit
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_bal_line_edit():
|
||||
from bal.gui.qt.common import shown_cv
|
||||
from bal.gui.qt.widgets import BalLineEdit
|
||||
cv = shown_cv("initial")
|
||||
edit = BalLineEdit(cv)
|
||||
assert edit.text() == "initial"
|
||||
cv.set("updated")
|
||||
assert cv.get() == "updated"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalTextEdit
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_bal_text_edit():
|
||||
from bal.gui.qt.common import shown_cv
|
||||
from bal.gui.qt.widgets import BalTextEdit
|
||||
cv = shown_cv("multi\nline")
|
||||
edit = BalTextEdit(cv)
|
||||
assert edit.toPlainText() == "multi\nline"
|
||||
cv.set("changed")
|
||||
assert cv.get() == "changed"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BalCheckBox
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_bal_check_box():
|
||||
from bal.gui.qt.common import shown_cv
|
||||
from bal.gui.qt.widgets import BalCheckBox
|
||||
cv = shown_cv(True)
|
||||
cb = BalCheckBox(cv)
|
||||
assert cb.isChecked() is True
|
||||
cv.set(False)
|
||||
assert cv.get() is False
|
||||
|
||||
|
||||
def test_bal_check_box_on_click():
|
||||
from bal.gui.qt.common import shown_cv
|
||||
from bal.gui.qt.widgets import BalCheckBox
|
||||
calls = []
|
||||
def handler():
|
||||
calls.append(1)
|
||||
cv = shown_cv(True)
|
||||
cb = BalCheckBox(cv, on_click=handler)
|
||||
cb.click()
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# _LockTimeEditor
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_locktime_editor_is_acceptable():
|
||||
from bal.gui.qt.widgets import _LockTimeEditor
|
||||
assert _LockTimeEditor.is_acceptable_locktime(100) is True
|
||||
assert _LockTimeEditor.is_acceptable_locktime(0) is True
|
||||
assert _LockTimeEditor.is_acceptable_locktime(-1) is False
|
||||
assert _LockTimeEditor.is_acceptable_locktime(None) is True
|
||||
|
||||
|
||||
def test_locktime_editor_is_acceptable_string():
|
||||
from bal.gui.qt.widgets import _LockTimeEditor
|
||||
assert _LockTimeEditor.is_acceptable_locktime("100") is True
|
||||
assert _LockTimeEditor.is_acceptable_locktime("abc") is False
|
||||
assert _LockTimeEditor.is_acceptable_locktime("") is True
|
||||
|
||||
|
||||
def test_locktime_editor_min_max():
|
||||
from bal.gui.qt.widgets import _LockTimeEditor
|
||||
assert _LockTimeEditor.min_allowed_value >= 0
|
||||
assert _LockTimeEditor.max_allowed_value > _LockTimeEditor.min_allowed_value
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# LockTimeRawEdit
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_locktime_raw_edit_replace_str():
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
assert LockTimeRawEdit.replace_str("123d") == "123"
|
||||
assert LockTimeRawEdit.replace_str("456y") == "456"
|
||||
assert LockTimeRawEdit.replace_str("789b") == "789"
|
||||
assert LockTimeRawEdit.replace_str("12d34y56b") == "123456"
|
||||
|
||||
|
||||
def test_locktime_raw_edit_checkbdy():
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
# character at expected position matches appendix
|
||||
pos, s = LockTimeRawEdit.checkbdy(None, "123d", 4, "d")
|
||||
assert s == "123d"
|
||||
# character at expected position does not match
|
||||
pos, s = LockTimeRawEdit.checkbdy(None, "123x", 4, "d")
|
||||
assert s == "123x"
|
||||
|
||||
|
||||
def test_locktime_raw_edit_numbify_empty():
|
||||
parent = QWidget()
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
edit = LockTimeRawEdit(parent)
|
||||
edit.setText("")
|
||||
edit.numbify()
|
||||
assert edit.text() == ""
|
||||
|
||||
|
||||
def test_locktime_raw_edit_numbify_days():
|
||||
parent = QWidget()
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
edit = LockTimeRawEdit(parent)
|
||||
# Use setText + numbify to simulate user typing
|
||||
edit.blockSignals(True)
|
||||
edit.setText("30d")
|
||||
edit.blockSignals(False)
|
||||
edit.numbify()
|
||||
# Should be "30d" with isdays=True
|
||||
assert edit.text() == "30d"
|
||||
|
||||
|
||||
def test_locktime_raw_edit_numbify_years():
|
||||
parent = QWidget()
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
edit = LockTimeRawEdit(parent)
|
||||
edit.blockSignals(True)
|
||||
edit.setText("2y")
|
||||
edit.blockSignals(False)
|
||||
edit.numbify()
|
||||
assert edit.text() == "2y"
|
||||
|
||||
|
||||
def test_locktime_raw_edit_get_set_value():
|
||||
parent = QWidget()
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
edit = LockTimeRawEdit(parent)
|
||||
edit.set_value("90d")
|
||||
val = edit.get_value()
|
||||
assert val is not None
|
||||
assert "d" in val
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# PercAmountEdit
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_perc_amount_edit_numbify_percent():
|
||||
from bal.gui.qt.widgets import PercAmountEdit
|
||||
parent = QWidget()
|
||||
edit = PercAmountEdit(8, parent=parent)
|
||||
edit.blockSignals(True)
|
||||
edit.setText("50%")
|
||||
edit.blockSignals(False)
|
||||
edit.numbify()
|
||||
assert edit.is_perc is True
|
||||
# After numbify: "50%" -> strip % -> add back -> "50%"
|
||||
assert edit.text() == "50%"
|
||||
|
||||
|
||||
def test_perc_amount_edit_numbify_no_percent():
|
||||
from bal.gui.qt.widgets import PercAmountEdit
|
||||
parent = QWidget()
|
||||
edit = PercAmountEdit(8, parent=parent)
|
||||
edit.blockSignals(True)
|
||||
edit.setText("123")
|
||||
edit.blockSignals(False)
|
||||
edit.numbify()
|
||||
assert edit.is_perc is False
|
||||
assert edit.text() == "123"
|
||||
|
||||
|
||||
def test_perc_amount_get_amount_from_text():
|
||||
from bal.gui.qt.widgets import PercAmountEdit
|
||||
parent = QWidget()
|
||||
edit = PercAmountEdit(8, parent=parent)
|
||||
# With percent
|
||||
result = edit._get_amount_from_text("50%")
|
||||
assert result is not None
|
||||
# Without percent
|
||||
result = edit._get_amount_from_text("123.45")
|
||||
assert result is not None
|
||||
# Invalid
|
||||
result = edit._get_amount_from_text("abc")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_perc_amount_get_text_from_amount():
|
||||
from bal.gui.qt.widgets import PercAmountEdit
|
||||
parent = QWidget()
|
||||
edit = PercAmountEdit(lambda: 8, parent=parent)
|
||||
edit.numbify() # sets is_perc
|
||||
text = edit._get_text_from_amount(100)
|
||||
assert isinstance(text, str)
|
||||
|
||||
|
||||
def test_perc_amount_get_text_from_amount_perc():
|
||||
from bal.gui.qt.widgets import PercAmountEdit
|
||||
parent = QWidget()
|
||||
edit = PercAmountEdit(lambda: 8, parent=parent)
|
||||
edit.blockSignals(True)
|
||||
edit.setText("50%")
|
||||
edit.blockSignals(False)
|
||||
edit.numbify() # sets is_perc = True
|
||||
text = edit._get_text_from_amount(100)
|
||||
assert "%" in text
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Main
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All widget tests passed")
|
||||
Reference in New Issue
Block a user