Files
bal-electrum-plugin/tests/windows_overflow_test.py
genspark-ai-developer[bot] 3c44a29f84 fix: crash GUI su Windows (OverflowError anno 2038) — schede/menu BAL rotti (#3)
* fix(gui): voci di menu BAL duplicate/condensate dopo riavvio o cambio wallet

Sintomo (Windows 11): dopo aver riavviato Electrum o cambiato wallet, le
schede Will/Heirs sparivano dalla tab bar e dal menu, e compariva una voce
di menu condensata/illeggibile (icona + testo sovrapposti) sotto il logo di
Electrum, accanto a 'Portafogli'.

Causa: init_menubar_tools veniva eseguito DUE volte sulla stessa finestra.
Con il plugin gia abilitato, al riavvio Electrum invoca sia l'hook
init_menubar sia il percorso di init a caldo (init_qt -> _setup_window),
entrambi chiamano init_menubar_tools -> addTab/addAction duplicati.
Nell'originale init_qt faceva return (chiedendo il riavvio) e quindi i menu
venivano creati una sola volta; rimuovendo quel return (fix B3) e' emersa la
doppia inizializzazione.

Fix:
- BalWindow._menubar_initialized: guardia di idempotenza.
- init_menubar_tools: se gia inizializzato, esce subito (niente duplicati).
- on_close: resetta il flag dopo aver rimosso tab/azioni, cosi la stessa
  finestra puo essere riusata per un altro wallet.
- tests/gui_fixes_test.py: regressione che verifica la guardia in __init__,
  init_menubar_tools e on_close.

Logica di business invariata (nessuna modifica a bal/core/*).

* fix(gui): ripristina create_status_bar come no-op (come originale)

L'elemento di menu condensato/illeggibile sotto il logo di Electrum era
causato dal StatusBarButton aggiunto da create_status_bar.

Nell'originale Gitea questo hook aveva un 'return' subito dopo il log, PRIMA
di costruire il bottone: era quindi disabilitato di proposito. Durante la
pulizia del 'dead code' nel refactoring quel return era stato rimosso,
riattivando la creazione del bottone -> elemento icona+testo renderizzato
nel punto sbagliato dopo riavvio/cambio wallet.

Fix: create_status_bar torna a essere un no-op (return), fedele all'originale.
Le impostazioni restano raggiungibili da Strumenti -> Plugin.

Regressione: gui_fixes_test verifica che create_status_bar non chiami
addPermanentWidget.

* fix(core): OverflowError su Windows (anno 2038) che rompeva tab/menu BAL

CAUSA VERA (dal log Electrum dell'utente, Windows 11):

  OverflowError: Python int too large to convert to C int
    window.py __init__ -> create_heirs_tab -> WillSettingsWidget
    -> on_locktime_change -> BalTimestamp.to_date
    -> datetime.fromtimestamp(NLOCKTIME_MAX)

Su Windows time_t e' a 32 bit, quindi datetime.fromtimestamp() solleva
OverflowError per qualsiasi timestamp oltre il 2038 (es. NLOCKTIME_MAX =
2**32-1 = 4294967295, usato come locktime di default/sentinella). Su Linux
64-bit la stessa chiamata funziona: per questo il bug si vedeva solo su
Windows e i test su Linux non lo intercettavano.

L'eccezione interrompeva BalWindow.__init__ durante init_menubar/load_wallet,
lasciando le schede Will/Heirs e la voce di menu a meta' costruzione ->
l'elemento grafico condensato/illeggibile sotto il logo di Electrum.

FIX (comportamento invariato per tutti i valori normali):
- BalTimestamp._safe_fromtimestamp(): datetime.fromtimestamp con clamp a
  INT32_MAX in caso di OverflowError/OSError/ValueError, esattamente come la
  funzione get_max_allowed_timestamp() dell'originale (Electrum issue #6170).
- Usato in to_date / to_timestamp / __str__ / __repr__ di BalTimestamp.
- widgets.py set_value: usa il converter sicuro.
- util.py timestamp_minus: stessa protezione inline con clamp a INT32_MAX.

I valori entro il 2038 (date assolute normali, durate relative come 90d/5y)
producono lo stesso identico risultato di prima.

TEST: tests/windows_overflow_test.py riproduce il limite 32-bit di Windows
(monkeypatch di datetime.fromtimestamp) e dimostra che senza il fix si ottiene
lo stesso OverflowError del log, mentre col fix passa. Verificato anche che il
test FALLISCE senza il fix.

* docs(it): documenta il fix OverflowError Windows (anno 2038) nel changelog

Aggiunge la sezione §13 al CHANGELOG_REFACTOR.md che descrive:
- sintomo (schede/menu rotti su Windows dopo riavvio/cambio wallet)
- causa vera dal log (datetime.fromtimestamp(NLOCKTIME_MAX) -> OverflowError
  su time_t 32-bit di Windows)
- fix con _safe_fromtimestamp (clamp a INT32_MAX, come Electrum #6170)
- test di regressione windows_overflow_test.py
Aggiornata anche la cronologia (§12) con PR #3.

---------

Co-authored-by: GenSpark AI Developer <ai@genspark.dev>
2026-06-13 16:29:30 +00:00

98 lines
3.8 KiB
Python

"""
Regression test for the Windows year-2038 OverflowError crash.
Background
----------
On Windows ``time_t`` is 32-bit, so ``datetime.fromtimestamp(ts)`` raises
``OverflowError: Python int too large to convert to C int`` for any timestamp
past 2038 (e.g. ``NLOCKTIME_MAX = 2**32 - 1``, used as the default/sentinel
locktime). On 64-bit Linux the same call succeeds, which is why the bug only
showed up on the user's Windows build: ``BalWindow.__init__`` ->
``create_heirs_tab`` -> ``WillSettingsWidget`` -> ``on_locktime_change`` ->
``BalTimestamp.to_date`` -> ``datetime.fromtimestamp(NLOCKTIME_MAX)`` crashed,
which aborted ``init_menubar`` / ``load_wallet`` and left the Will/Heirs tabs
and the menu entry half-built (the garbled/condensed element under the logo).
This test forces ``datetime.fromtimestamp`` to behave like the Windows 32-bit
implementation, then exercises ``BalTimestamp`` with NLOCKTIME_MAX to prove the
overflow-safe conversion no longer raises and clamps to INT32_MAX.
Run with:
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
python3 tests/windows_overflow_test.py <PLUGIN_IMPORT_NAME>
"""
import datetime as _datetime_mod
import importlib
import sys
PKG = sys.argv[1] if len(sys.argv) > 1 else "electrum.plugins.bal"
INT32_MAX = 2 ** 31 - 1
NLOCKTIME_MAX = 2 ** 32 - 1 # 4294967295, the value seen in the crash log
_real_datetime = _datetime_mod.datetime
class _WindowsLikeDatetime(_real_datetime):
"""A datetime subclass whose fromtimestamp emulates Windows' 32-bit limit."""
@classmethod
def fromtimestamp(cls, ts, tz=None):
if tz is None and (ts > INT32_MAX or ts < 0):
raise OverflowError("Python int too large to convert to C int")
return _real_datetime.fromtimestamp(ts, tz)
def main():
plugin_base = importlib.import_module(f"{PKG}.core.plugin_base")
BalTimestamp = plugin_base.BalTimestamp
# 1) Sanity: with the real (Linux 64-bit) datetime, large ts works already.
bt = BalTimestamp(NLOCKTIME_MAX)
d = bt.to_date()
assert isinstance(d, _real_datetime), d
print("[OK] BalTimestamp(NLOCKTIME_MAX).to_date() works on this platform")
# 2) Now emulate Windows: patch datetime in the plugin_base module so that
# fromtimestamp raises OverflowError past 2038, exactly like Windows.
original = plugin_base.datetime
plugin_base.datetime = _WindowsLikeDatetime
try:
# 2a) Absolute sentinel timestamp (the exact crash path from the log).
bt = BalTimestamp(NLOCKTIME_MAX)
d = bt.to_date() # must NOT raise OverflowError anymore
assert d.year <= 2038, f"expected clamp to <=2038, got {d!r}"
print("[OK] to_date(NLOCKTIME_MAX) no longer raises (clamped to INT32_MAX)")
# 2b) to_timestamp must also be safe.
ts = bt.to_timestamp()
assert ts <= INT32_MAX, ts
print("[OK] to_timestamp(NLOCKTIME_MAX) clamped & safe")
# 2c) __str__ / __repr__ must not raise either.
_ = str(bt)
_ = repr(bt)
print("[OK] str()/repr() on out-of-range timestamp are safe")
# 2d) Relative durations that overflow when added (e.g. huge 'd').
bt_rel = BalTimestamp(f"{10 ** 9}d") # ~2.7M years -> overflow
d2 = bt_rel.to_date()
assert d2 is not None
print("[OK] huge relative duration no longer raises")
# 2e) Normal values are unchanged (behaviour-preserving check).
bt_norm = BalTimestamp("90d")
d3 = bt_norm.to_date()
# 90 days from now, normalised to midnight
assert d3.hour == 0 and d3.minute == 0 and d3.second == 0
print("[OK] normal '90d' value still resolves to a midnight datetime")
finally:
plugin_base.datetime = original
print(f"\n[OK] Windows overflow regression passed for package {PKG!r}")
return 0
if __name__ == "__main__":
sys.exit(main())