Files
bal-electrum-plugin/tests/test_gui_widgets.py
GenSpark AI Developer 03985a2566 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.
2026-06-28 22:51:28 -04:00

256 lines
7.7 KiB
Python

"""
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")