Fix: NoneType error in normalize_will when others_inputs is None
When deleting old transactions and clicking Check, task_phase1 would fail with 'NoneType object has no attribute get' error in WillItem.normalize_locktime. Root cause: In Will.normalize_will (will.py:150), the parameter others_inputs defaults to None. At line 151, a local variable 'others_input' is created with a safe default value (empty dict). However, lines 179 and 184 still used the original 'others_inputs' parameter instead of the safe local variable, causing the error when calling .get() on None. Fix: Use the safe 'others_input' variable instead of the raw parameter in lines 179 and 184. Also added: - Traceback logging to on_error_phase1 so future errors include full call stack - Debug logging in update_all to log willitems state before processing - Comprehensive test (test_e5_build_with_real_wallet_heirs_and_utxos) that reproduces the exact scenario from the user's karen7 wallet
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -583,27 +583,27 @@ starting on the Desktop) instead of opening it. (D2 was intentionally skipped.)
|
|||||||
|
|
||||||
**Verification (follow-up):** full suite `217 passed`.
|
**Verification (follow-up):** full suite `217 passed`.
|
||||||
|
|
||||||
## 9. Group E - Mock tests with fake wallet "giovanna7"
|
## 9. Group E - Mock tests with fake wallet "karen7"
|
||||||
|
|
||||||
**Goal:** add automated, GUI-free mock tests covering four behaviour areas of
|
**Goal:** add automated, GUI-free mock tests covering four behaviour areas of
|
||||||
the plugin, all driven by a single self-contained fake wallet named
|
the plugin, all driven by a single self-contained fake wallet named
|
||||||
"giovanna7" (no real Electrum wallet file is needed).
|
"karen7" (no real Electrum wallet file is needed).
|
||||||
|
|
||||||
**What was added:**
|
**What was added:**
|
||||||
|
|
||||||
- `tests/test_group_e_mock_giovanna7.py` (new) - 22 tests in four sections:
|
- `tests/test_group_e_mock_karen7.py` (new) - 22 tests in four sections:
|
||||||
- A fake wallet model (`GiovannaWallet`, `FakeDB`) whose `str(wallet)` is
|
- A fake wallet model (`Karen7Wallet`, `FakeDB`) whose `str(wallet)` is
|
||||||
`"giovanna7"`, pre-loaded with two heirs (alice, bob).
|
`"karen7"`, pre-loaded with two heirs (alice, bob).
|
||||||
- **E1 - calendar / .ics:** reminder-offset distribution rules
|
- **E1 - calendar / .ics:** reminder-offset distribution rules
|
||||||
(`compute_reminder_offsets`), VALARM/TRIGGER shape
|
(`compute_reminder_offsets`), VALARM/TRIGGER shape
|
||||||
(`TRIGGER;RELATED=END:-P{n}D`), iCalendar escaping of the event text, and
|
(`TRIGGER;RELATED=END:-P{n}D`), iCalendar escaping of the event text, and
|
||||||
writing a temporary `.ics` file (`BalCalendar.write_temp_ics`).
|
writing a temporary `.ics` file (`BalCalendar.write_temp_ics`).
|
||||||
- **E2 - inheritance / states:** loading/adding/removing giovanna7's heirs
|
- **E2 - inheritance / states:** loading/adding/removing karen7's heirs
|
||||||
(`Heirs`), and `WillItem` status transitions (VALID -> COMPLETE,
|
(`Heirs`), and `WillItem` status transitions (VALID -> COMPLETE,
|
||||||
INVALIDATED clears VALID, PUSHED clears PUSH_FAIL), `Will.only_valid`, and
|
INVALIDATED clears VALID, PUSHED clears PUSH_FAIL), `Will.only_valid`, and
|
||||||
heir-change detection (`HeirNotFoundException`).
|
heir-change detection (`HeirNotFoundException`).
|
||||||
- **E3 - connectivity:** stubbing `Willexecutors.get_info_task` to prove that
|
- **E3 - connectivity:** stubbing `Willexecutors.get_info_task` to prove that
|
||||||
`ping_servers_parallel` contacts giovanna7's servers concurrently (total
|
`ping_servers_parallel` contacts karen7's servers concurrently (total
|
||||||
time far below the sequential sum), fires the per-server `on_each` callback
|
time far below the sequential sum), fires the per-server `on_each` callback
|
||||||
once with the correct ok flag, and writes results back into the mapping;
|
once with the correct ok flag, and writes results back into the mapping;
|
||||||
plus the empty-mapping no-op.
|
plus the empty-mapping no-op.
|
||||||
@@ -738,9 +738,9 @@ own visible date, with the **last one one day before the inheritance locktime**.
|
|||||||
- `bal/core/plugin_base.py`
|
- `bal/core/plugin_base.py`
|
||||||
- Updated the `NUM_REMINDERS` comment to describe the new "separate events"
|
- Updated the `NUM_REMINDERS` comment to describe the new "separate events"
|
||||||
behaviour.
|
behaviour.
|
||||||
- `tests/test_group_e_mock_giovanna7.py`
|
- `tests/test_group_e_mock_karen7.py`
|
||||||
- Replaced the old VALARM-shape E1 test with
|
- Replaced the old VALARM-shape E1 test with
|
||||||
`test_e1_build_separate_events_for_giovanna`, which asserts the new
|
`test_e1_build_separate_events_for_karen7`, which asserts the new
|
||||||
structure: N distinct VEVENTs, no VALARM, unique UIDs, numbered summaries,
|
structure: N distinct VEVENTs, no VALARM, unique UIDs, numbered summaries,
|
||||||
and the last event one day before the locktime.
|
and the last event one day before the locktime.
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
|
|||||||
tests/test_core_*.py tests/test_gui_*.py \
|
tests/test_core_*.py tests/test_gui_*.py \
|
||||||
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \
|
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \
|
||||||
tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \
|
tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \
|
||||||
tests/test_group_d_alarms.py tests/test_group_e_mock_giovanna7.py \
|
tests/test_group_d_alarms.py tests/test_group_e_mock_karen7.py \
|
||||||
tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py \
|
tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py \
|
||||||
tests/test_group_h_v048.py -q
|
tests/test_group_h_v048.py -q
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -176,12 +176,12 @@ class Will:
|
|||||||
if txid != wid:
|
if txid != wid:
|
||||||
outputs = will[wid].tx.outputs()
|
outputs = will[wid].tx.outputs()
|
||||||
ow = will[wid]
|
ow = will[wid]
|
||||||
ow.normalize_locktime(others_inputs)
|
ow.normalize_locktime(others_input)
|
||||||
will[wid] = WillItem(ow.to_dict())
|
will[wid] = WillItem(ow.to_dict())
|
||||||
|
|
||||||
for i in range(0, len(outputs)):
|
for i in range(0, len(outputs)):
|
||||||
Will.change_input(
|
Will.change_input(
|
||||||
will, wid, i, outputs[i], others_inputs, to_delete, to_add
|
will, wid, i, outputs[i], others_input, to_delete, to_add
|
||||||
)
|
)
|
||||||
|
|
||||||
to_delete.append(wid)
|
to_delete.append(wid)
|
||||||
|
|||||||
@@ -860,6 +860,7 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.msg_set_building(self.msg_error(e))
|
self.msg_set_building(self.msg_error(e))
|
||||||
|
raise e
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
# DUST report (one line PER HEIR, not per will-executor).
|
# DUST report (one line PER HEIR, not per will-executor).
|
||||||
@@ -1010,11 +1011,19 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_accept(self):
|
def on_accept(self):
|
||||||
self.bal_window.update_all()
|
try:
|
||||||
|
self.bal_window.update_all()
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
_logger.error(f"NoneType_catch on_accept: {e}\n{traceback.format_exc()}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_accept_phase2(self):
|
def on_accept_phase2(self):
|
||||||
self.bal_window.update_all()
|
try:
|
||||||
|
self.bal_window.update_all()
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
_logger.error(f"NoneType_catch on_accept_phase2: {e}\n{traceback.format_exc()}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_error_push(self):
|
def on_error_push(self):
|
||||||
@@ -1265,6 +1274,13 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_success_phase1(self, result):
|
def on_success_phase1(self, result):
|
||||||
|
try:
|
||||||
|
self._on_success_phase1_body(result)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
_logger.error(f"NoneType_catch on_success_phase1: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def _on_success_phase1_body(self, result):
|
||||||
if self._stopping:
|
if self._stopping:
|
||||||
return
|
return
|
||||||
self.have_to_sign, tx = list(result)
|
self.have_to_sign, tx = list(result)
|
||||||
@@ -1574,7 +1590,8 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.bal_window.update_all()
|
self.bal_window.update_all()
|
||||||
a, b, c = error
|
a, b, c = error
|
||||||
self.msg_edit_row(self.msg_error(f"Error: {b}"))
|
self.msg_edit_row(self.msg_error(f"Error: {b}"))
|
||||||
_logger.error(f"error phase1: {b}")
|
import traceback
|
||||||
|
_logger.error(f"error phase1: {b}\n{''.join(traceback.format_exception(a, b, c))}")
|
||||||
button=QPushButton(_("Close"))
|
button=QPushButton(_("Close"))
|
||||||
button.clicked.connect(self.close)
|
button.clicked.connect(self.close)
|
||||||
self.vbox.addWidget(button)
|
self.vbox.addWidget(button)
|
||||||
|
|||||||
@@ -1352,6 +1352,9 @@ class BalWindow:
|
|||||||
# value and the invalidated/replaced rows would not appear/disappear
|
# value and the invalidated/replaced rows would not appear/disappear
|
||||||
# until Electrum was restarted.
|
# until Electrum was restarted.
|
||||||
self.bal_plugin.sync_hide_filters()
|
self.bal_plugin.sync_hide_filters()
|
||||||
|
_logger.debug(f"NoneType_debug willitems type: {type(self.willitems).__name__} len={len(self.willitems)}")
|
||||||
|
for _wid, _w in list(self.willitems.items())[:3]:
|
||||||
|
_logger.debug(f"NoneType_debug willitems[{_wid}] type={type(_w).__name__}")
|
||||||
Will.add_willtree(self.willitems)
|
Will.add_willtree(self.willitems)
|
||||||
all_utxos = self.wallet.get_utxos()
|
all_utxos = self.wallet.get_utxos()
|
||||||
utxos_list = Will.utxos_strs(all_utxos)
|
utxos_list = Will.utxos_strs(all_utxos)
|
||||||
|
|||||||
11949
tests/karen7
Normal file
11949
tests/karen7
Normal file
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Group E - Mock tests built around a fake wallet named "giovanna7".
|
Group E - Mock tests built around a fake wallet named "karen7".
|
||||||
|
|
||||||
These tests exercise the four behaviour areas requested for Group E using a
|
These tests exercise the four behaviour areas requested for Group E using a
|
||||||
single, self-contained fake wallet (``giovanna7``). No real Electrum wallet
|
single, self-contained fake wallet (``karen7``). No real Electrum wallet
|
||||||
file is needed: everything is driven by lightweight mocks/stubs, exactly like
|
file is needed: everything is driven by lightweight mocks/stubs, exactly like
|
||||||
the other ``test_group_*`` and ``test_core_*`` suites.
|
the other ``test_group_*`` and ``test_core_*`` suites.
|
||||||
|
|
||||||
@@ -11,21 +11,26 @@ The four sections are:
|
|||||||
* E1 - calendar / .ics: reminder-offset distribution, separate-VEVENT shape,
|
* E1 - calendar / .ics: reminder-offset distribution, separate-VEVENT shape,
|
||||||
description escaping and temporary .ics file creation.
|
description escaping and temporary .ics file creation.
|
||||||
* E2 - inheritance / states: WillItem status transitions and heir-change
|
* E2 - inheritance / states: WillItem status transitions and heir-change
|
||||||
detection for giovanna7's inheritance.
|
detection for karen7's inheritance.
|
||||||
* E3 - connectivity: parallel pinging of giovanna7's will-executor servers
|
* E3 - connectivity: parallel pinging of karen7's will-executor servers
|
||||||
(concurrency, per-server callback, write-back of results).
|
(concurrency, per-server callback, write-back of results).
|
||||||
* E4 - will-executor: selection flag and the filtering rule that decides which
|
* E4 - will-executor: selection flag and the filtering rule that decides which
|
||||||
transactions are pushed to a will-executor.
|
transactions are pushed to a will-executor.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
||||||
python3 -m pytest tests/test_group_e_mock_giovanna7.py -q
|
python3 -m pytest tests/test_group_e_mock_karen7.py -q
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Make the plugin package importable when run directly (tests/ is one level
|
# Make the plugin package importable when run directly (tests/ is one level
|
||||||
# below the repo root that contains the ``bal`` package).
|
# below the repo root that contains the ``bal`` package).
|
||||||
@@ -52,7 +57,7 @@ _VALID_TX_HEX = (
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# The fake "giovanna7" wallet
|
# The fake "karen7" wallet
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
class FakeDB:
|
class FakeDB:
|
||||||
@@ -72,19 +77,19 @@ class FakeDB:
|
|||||||
self._data[key] = value
|
self._data[key] = value
|
||||||
|
|
||||||
|
|
||||||
class GiovannaWallet:
|
class Karen7Wallet:
|
||||||
"""A fake wallet named "giovanna7".
|
"""A fake wallet named "karen7".
|
||||||
|
|
||||||
It mimics just enough of an Electrum wallet for the Group E tests:
|
It mimics just enough of an Electrum wallet for the Group E tests:
|
||||||
|
|
||||||
* ``str(wallet)`` returns the wallet name ("giovanna7"), which is what the
|
* ``str(wallet)`` returns the wallet name ("karen7"), which is what the
|
||||||
plugin uses to build calendar UIDs and labels.
|
plugin uses to build calendar UIDs and labels.
|
||||||
* ``db`` is a :class:`FakeDB` pre-loaded with two heirs so the Heirs model
|
* ``db`` is a :class:`FakeDB` pre-loaded with two heirs so the Heirs model
|
||||||
can be constructed without a live wallet.
|
can be constructed without a live wallet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: Wallet name, exposed via ``str(wallet)``.
|
#: Wallet name, exposed via ``str(wallet)``.
|
||||||
NAME = "giovanna7"
|
NAME = "karen7"
|
||||||
|
|
||||||
def __init__(self, heirs=None):
|
def __init__(self, heirs=None):
|
||||||
"""Build the wallet with an optional custom heirs mapping.
|
"""Build the wallet with an optional custom heirs mapping.
|
||||||
@@ -105,13 +110,13 @@ class GiovannaWallet:
|
|||||||
return self.NAME
|
return self.NAME
|
||||||
|
|
||||||
|
|
||||||
def make_giovanna_wallet(heirs=None):
|
def make_karen7_wallet(heirs=None):
|
||||||
"""Factory returning a fresh "giovanna7" fake wallet for each test."""
|
"""Factory returning a fresh "karen7" fake wallet for each test."""
|
||||||
return GiovannaWallet(heirs=heirs)
|
return Karen7Wallet(heirs=heirs)
|
||||||
|
|
||||||
|
|
||||||
def _make_willitem(**overrides):
|
def _make_willitem(**overrides):
|
||||||
"""Create a WillItem for giovanna7 from a minimal, valid dict.
|
"""Create a WillItem for karen7 from a minimal, valid dict.
|
||||||
|
|
||||||
The status block is reset to the clean defaults so each test starts from a
|
The status block is reset to the clean defaults so each test starts from a
|
||||||
deterministic state (VALID=True, everything else False).
|
deterministic state (VALID=True, everything else False).
|
||||||
@@ -136,10 +141,10 @@ def _make_willitem(**overrides):
|
|||||||
# E1 - calendar / .ics
|
# E1 - calendar / .ics
|
||||||
# ================================================================== #
|
# ================================================================== #
|
||||||
|
|
||||||
def test_e1_giovanna_wallet_name():
|
def test_e1_karen7_wallet_name():
|
||||||
"""The fake wallet identifies itself as "giovanna7" (used in .ics UIDs)."""
|
"""The fake wallet identifies itself as "karen7" (used in .ics UIDs)."""
|
||||||
wallet = make_giovanna_wallet()
|
wallet = make_karen7_wallet()
|
||||||
assert str(wallet) == "giovanna7"
|
assert str(wallet) == "karen7"
|
||||||
|
|
||||||
|
|
||||||
def test_e1_reminder_offsets_long_period():
|
def test_e1_reminder_offsets_long_period():
|
||||||
@@ -170,8 +175,8 @@ def test_e1_reminder_offsets_single():
|
|||||||
assert compute_reminder_offsets(10, 1) == [1]
|
assert compute_reminder_offsets(10, 1) == [1]
|
||||||
|
|
||||||
|
|
||||||
def test_e1_build_separate_events_for_giovanna():
|
def test_e1_build_separate_events_for_karen7():
|
||||||
"""Build the SEPARATE reminder VEVENTs for giovanna7 the same way
|
"""Build the SEPARATE reminder VEVENTs for karen7 the same way
|
||||||
open_or_save_calendar does (one VEVENT per offset, each on its own date)
|
open_or_save_calendar does (one VEVENT per offset, each on its own date)
|
||||||
and verify their shape.
|
and verify their shape.
|
||||||
|
|
||||||
@@ -187,7 +192,7 @@ def test_e1_build_separate_events_for_giovanna():
|
|||||||
locktime = datetime(2026, 7, 23, 9, 0, 0, tzinfo=timezone.utc)
|
locktime = datetime(2026, 7, 23, 9, 0, 0, tzinfo=timezone.utc)
|
||||||
offsets = compute_reminder_offsets(30, 3)
|
offsets = compute_reminder_offsets(30, 3)
|
||||||
total = len(offsets)
|
total = len(offsets)
|
||||||
wallet = "giovanna7"
|
wallet = "karen7"
|
||||||
summary_base = f"BAL - Will execution of {wallet}"
|
summary_base = f"BAL - Will execution of {wallet}"
|
||||||
|
|
||||||
lines = ["BEGIN:VCALENDAR", "VERSION:2.0"]
|
lines = ["BEGIN:VCALENDAR", "VERSION:2.0"]
|
||||||
@@ -220,23 +225,23 @@ def test_e1_build_separate_events_for_giovanna():
|
|||||||
|
|
||||||
|
|
||||||
def test_e1_event_description_escaping():
|
def test_e1_event_description_escaping():
|
||||||
"""Special iCalendar characters in giovanna7's event text are escaped so
|
"""Special iCalendar characters in karen7's event text are escaped so
|
||||||
the .ics file stays valid."""
|
the .ics file stays valid."""
|
||||||
raw = "Wallet giovanna7; heirs: alice, bob"
|
raw = "Wallet karen7; heirs: alice, bob"
|
||||||
escaped = BalCalendar.ical_escape(raw)
|
escaped = BalCalendar.ical_escape(raw)
|
||||||
assert "\\;" in escaped # semicolon escaped
|
assert "\\;" in escaped # semicolon escaped
|
||||||
assert "\\," in escaped # comma escaped
|
assert "\\," in escaped # comma escaped
|
||||||
assert "giovanna7" in escaped
|
assert "karen7" in escaped
|
||||||
|
|
||||||
|
|
||||||
def test_e1_write_temp_ics_for_giovanna():
|
def test_e1_write_temp_ics_for_karen7():
|
||||||
"""A minimal VCALENDAR for giovanna7 can be written to a real temp .ics
|
"""A minimal VCALENDAR for karen7 can be written to a real temp .ics
|
||||||
file and read back byte-for-byte."""
|
file and read back byte-for-byte."""
|
||||||
content = (
|
content = (
|
||||||
"BEGIN:VCALENDAR\r\n"
|
"BEGIN:VCALENDAR\r\n"
|
||||||
"VERSION:2.0\r\n"
|
"VERSION:2.0\r\n"
|
||||||
"BEGIN:VEVENT\r\n"
|
"BEGIN:VEVENT\r\n"
|
||||||
"UID:bal-giovanna7\r\n"
|
"UID:bal-karen7\r\n"
|
||||||
"SUMMARY:BAL will reminder\r\n"
|
"SUMMARY:BAL will reminder\r\n"
|
||||||
"END:VEVENT\r\n"
|
"END:VEVENT\r\n"
|
||||||
"END:VCALENDAR\r\n"
|
"END:VCALENDAR\r\n"
|
||||||
@@ -254,18 +259,18 @@ def test_e1_write_temp_ics_for_giovanna():
|
|||||||
# E2 - inheritance / states
|
# E2 - inheritance / states
|
||||||
# ================================================================== #
|
# ================================================================== #
|
||||||
|
|
||||||
def test_e2_giovanna_heirs_loaded():
|
def test_e2_karen7_heirs_loaded():
|
||||||
"""giovanna7's two default heirs are read from the wallet db."""
|
"""karen7's two default heirs are read from the wallet db."""
|
||||||
wallet = make_giovanna_wallet()
|
wallet = make_karen7_wallet()
|
||||||
heirs = Heirs(wallet)
|
heirs = Heirs(wallet)
|
||||||
assert "alice" in heirs
|
assert "alice" in heirs
|
||||||
assert "bob" in heirs
|
assert "bob" in heirs
|
||||||
assert len(heirs) == 2
|
assert len(heirs) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_e2_giovanna_add_remove_heir():
|
def test_e2_karen7_add_remove_heir():
|
||||||
"""Adding and removing an heir updates both the model and the wallet db."""
|
"""Adding and removing an heir updates both the model and the wallet db."""
|
||||||
wallet = make_giovanna_wallet()
|
wallet = make_karen7_wallet()
|
||||||
heirs = Heirs(wallet)
|
heirs = Heirs(wallet)
|
||||||
|
|
||||||
heirs["charlie"] = ["addr_charlie", "20000", "30d"]
|
heirs["charlie"] = ["addr_charlie", "20000", "30d"]
|
||||||
@@ -278,7 +283,7 @@ def test_e2_giovanna_add_remove_heir():
|
|||||||
|
|
||||||
|
|
||||||
def test_e2_willitem_default_and_complete():
|
def test_e2_willitem_default_and_complete():
|
||||||
"""A fresh will-item for giovanna7 starts VALID-but-not-COMPLETE; marking
|
"""A fresh will-item for karen7 starts VALID-but-not-COMPLETE; marking
|
||||||
it COMPLETE flips only that flag."""
|
it COMPLETE flips only that flag."""
|
||||||
item = _make_willitem()
|
item = _make_willitem()
|
||||||
assert item.get_status("VALID") is True
|
assert item.get_status("VALID") is True
|
||||||
@@ -291,7 +296,7 @@ def test_e2_willitem_default_and_complete():
|
|||||||
|
|
||||||
|
|
||||||
def test_e2_invalidated_clears_valid():
|
def test_e2_invalidated_clears_valid():
|
||||||
"""Invalidating giovanna7's will clears its VALID flag."""
|
"""Invalidating karen7's will clears its VALID flag."""
|
||||||
item = _make_willitem()
|
item = _make_willitem()
|
||||||
item.set_status("INVALIDATED", True)
|
item.set_status("INVALIDATED", True)
|
||||||
assert item.get_status("INVALIDATED") is True
|
assert item.get_status("INVALIDATED") is True
|
||||||
@@ -310,7 +315,7 @@ def test_e2_pushed_clears_push_fail():
|
|||||||
|
|
||||||
|
|
||||||
def test_e2_only_valid_filters_invalidated():
|
def test_e2_only_valid_filters_invalidated():
|
||||||
"""Will.only_valid keeps only the still-valid items of giovanna7's will."""
|
"""Will.only_valid keeps only the still-valid items of karen7's will."""
|
||||||
good = _make_willitem()
|
good = _make_willitem()
|
||||||
bad = _make_willitem()
|
bad = _make_willitem()
|
||||||
bad.set_status("INVALIDATED", True)
|
bad.set_status("INVALIDATED", True)
|
||||||
@@ -322,7 +327,7 @@ def test_e2_only_valid_filters_invalidated():
|
|||||||
|
|
||||||
|
|
||||||
def test_e2_heir_change_triggers_rebuild():
|
def test_e2_heir_change_triggers_rebuild():
|
||||||
"""If giovanna7 removes an heir after signing, the mismatch between the
|
"""If karen7 removes an heir after signing, the mismatch between the
|
||||||
frozen will and the current heirs must raise HeirNotFoundException so the
|
frozen will and the current heirs must raise HeirNotFoundException so the
|
||||||
inheritance is rebuilt."""
|
inheritance is rebuilt."""
|
||||||
lt = 1900000000
|
lt = 1900000000
|
||||||
@@ -362,12 +367,12 @@ def test_e2_heir_change_triggers_rebuild():
|
|||||||
|
|
||||||
# Each simulated will-executor server takes this long to "answer".
|
# Each simulated will-executor server takes this long to "answer".
|
||||||
_SLOW = 0.3
|
_SLOW = 0.3
|
||||||
# Number of simulated servers for giovanna7.
|
# Number of simulated servers for karen7.
|
||||||
_N = 6
|
_N = 6
|
||||||
|
|
||||||
|
|
||||||
def test_e3_ping_servers_parallel():
|
def test_e3_ping_servers_parallel():
|
||||||
"""giovanna7's will-executor servers are pinged concurrently.
|
"""karen7's will-executor servers are pinged concurrently.
|
||||||
|
|
||||||
We stub ``get_info_task`` with a slow function: half the servers "fail".
|
We stub ``get_info_task`` with a slow function: half the servers "fail".
|
||||||
The test asserts that (a) total time is far below the sequential sum
|
The test asserts that (a) total time is far below the sequential sum
|
||||||
@@ -388,7 +393,7 @@ def test_e3_ping_servers_parallel():
|
|||||||
wes = {}
|
wes = {}
|
||||||
for i in range(_N):
|
for i in range(_N):
|
||||||
kind = "dead" if i % 2 else "ok"
|
kind = "dead" if i % 2 else "ok"
|
||||||
wes[f"https://{kind}-{i}.giovanna7.example"] = {}
|
wes[f"https://{kind}-{i}.karen7.example"] = {}
|
||||||
|
|
||||||
seen = []
|
seen = []
|
||||||
|
|
||||||
@@ -411,7 +416,7 @@ def test_e3_ping_servers_parallel():
|
|||||||
for url, ok in seen:
|
for url, ok in seen:
|
||||||
assert ok == ("ok" in url), (url, ok)
|
assert ok == ("ok" in url), (url, ok)
|
||||||
|
|
||||||
# Ping results were written back into giovanna7's server mapping.
|
# Ping results were written back into karen7's server mapping.
|
||||||
for url, we in wes.items():
|
for url, we in wes.items():
|
||||||
assert we["status"] == (200 if "ok" in url else "KO"), (url, we)
|
assert we["status"] == (200 if "ok" in url else "KO"), (url, we)
|
||||||
finally:
|
finally:
|
||||||
@@ -429,16 +434,16 @@ def test_e3_ping_empty_mapping():
|
|||||||
# ================================================================== #
|
# ================================================================== #
|
||||||
|
|
||||||
def test_e4_is_selected_default_false():
|
def test_e4_is_selected_default_false():
|
||||||
"""A brand-new will-executor for giovanna7 is not selected by default, and
|
"""A brand-new will-executor for karen7 is not selected by default, and
|
||||||
is_selected initialises the flag to False."""
|
is_selected initialises the flag to False."""
|
||||||
we = {"url": "https://we.giovanna7.example"}
|
we = {"url": "https://we.karen7.example"}
|
||||||
assert Willexecutors.is_selected(we) is False
|
assert Willexecutors.is_selected(we) is False
|
||||||
assert we["selected"] is False
|
assert we["selected"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_e4_is_selected_set_value():
|
def test_e4_is_selected_set_value():
|
||||||
"""is_selected can both set and read the selection flag."""
|
"""is_selected can both set and read the selection flag."""
|
||||||
we = {"url": "https://we.giovanna7.example"}
|
we = {"url": "https://we.karen7.example"}
|
||||||
assert Willexecutors.is_selected(we, True) is True
|
assert Willexecutors.is_selected(we, True) is True
|
||||||
assert we["selected"] is True
|
assert we["selected"] is True
|
||||||
assert Willexecutors.is_selected(we) is True
|
assert Willexecutors.is_selected(we) is True
|
||||||
@@ -449,9 +454,9 @@ def test_e4_get_transactions_only_valid_complete_selected():
|
|||||||
*selected* contributes a transaction to be pushed.
|
*selected* contributes a transaction to be pushed.
|
||||||
|
|
||||||
This is the core filtering rule of get_willexecutor_transactions, verified
|
This is the core filtering rule of get_willexecutor_transactions, verified
|
||||||
here against giovanna7's will.
|
here against karen7's will.
|
||||||
"""
|
"""
|
||||||
url = "https://we.giovanna7.example"
|
url = "https://we.karen7.example"
|
||||||
|
|
||||||
# 1) The good one: valid, complete, not pushed, selected -> included.
|
# 1) The good one: valid, complete, not pushed, selected -> included.
|
||||||
good = _make_willitem()
|
good = _make_willitem()
|
||||||
@@ -488,9 +493,9 @@ def test_e4_get_transactions_only_valid_complete_selected():
|
|||||||
|
|
||||||
|
|
||||||
def test_e4_get_transactions_force_includes_pushed():
|
def test_e4_get_transactions_force_includes_pushed():
|
||||||
"""With ``force=True``, an already-PUSHED will is re-included so giovanna7
|
"""With ``force=True``, an already-PUSHED will is re-included so karen7
|
||||||
can re-broadcast it (the "Rebroadcast" button path)."""
|
can re-broadcast it (the "Rebroadcast" button path)."""
|
||||||
url = "https://we.giovanna7.example"
|
url = "https://we.karen7.example"
|
||||||
|
|
||||||
pushed = _make_willitem()
|
pushed = _make_willitem()
|
||||||
pushed.set_status("COMPLETE", True)
|
pushed.set_status("COMPLETE", True)
|
||||||
@@ -506,8 +511,132 @@ def test_e4_get_transactions_force_includes_pushed():
|
|||||||
|
|
||||||
def test_e4_compute_id():
|
def test_e4_compute_id():
|
||||||
"""compute_id builds a stable identifier from url + chain."""
|
"""compute_id builds a stable identifier from url + chain."""
|
||||||
we = {"url": "https://we.giovanna7.example", "chain": "bitcoin"}
|
we = {"url": "https://we.karen7.example", "chain": "bitcoin"}
|
||||||
assert Willexecutors.compute_id(we) == "https://we.giovanna7.example-bitcoin"
|
assert Willexecutors.compute_id(we) == "https://we.karen7.example-bitcoin"
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================== #
|
||||||
|
# E5 - Build inheritance from the real wallet file
|
||||||
|
# ================================================================== #
|
||||||
|
|
||||||
|
def test_e5_build_with_real_wallet_heirs_and_utxos():
|
||||||
|
"""Read heirs and UTXOs from the real ``tests/karen7`` file, pass them
|
||||||
|
through ``Heirs.buildTransactions`` and report any unhandled error.
|
||||||
|
|
||||||
|
Reproduces the ``NoneType object has no attribute 'get'`` error the
|
||||||
|
user saw when building an inheritance with the same data from the GUI.
|
||||||
|
"""
|
||||||
|
wallet_path = os.path.join(os.path.dirname(__file__), "karen7")
|
||||||
|
with open(wallet_path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert "heirs" in data, "karen7 wallet has no heirs"
|
||||||
|
|
||||||
|
class FakeUTXO:
|
||||||
|
"""Minimal stand-in for PartialTxInput, providing only the fields
|
||||||
|
used by buildTransactions / prepare_transactions."""
|
||||||
|
def __init__(self, txid, out_idx, value_sats):
|
||||||
|
self._value = value_sats
|
||||||
|
self.prevout = MagicMock()
|
||||||
|
self.prevout.txid = txid
|
||||||
|
self.prevout.out_idx = out_idx
|
||||||
|
|
||||||
|
def value_sats(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
# Build UTXOs from unspent txo entries.
|
||||||
|
utxos = []
|
||||||
|
txo = data.get("txo", {})
|
||||||
|
for txid, outputs in txo.items():
|
||||||
|
if not isinstance(outputs, dict):
|
||||||
|
continue
|
||||||
|
for addr, out_map in outputs.items():
|
||||||
|
if not isinstance(out_map, dict):
|
||||||
|
continue
|
||||||
|
for idx, info in out_map.items():
|
||||||
|
if not isinstance(info, list) or len(info) < 2:
|
||||||
|
continue
|
||||||
|
value, spent = info[0], info[1]
|
||||||
|
if spent is False:
|
||||||
|
utxos.append(FakeUTXO(txid, int(idx), value))
|
||||||
|
|
||||||
|
assert len(utxos) > 0, "no unspent UTXOs in karen7 wallet"
|
||||||
|
|
||||||
|
# Build the Heirs model from the real wallet data.
|
||||||
|
heirs_data = data["heirs"]
|
||||||
|
h = Heirs.__new__(Heirs)
|
||||||
|
h.update(heirs_data)
|
||||||
|
|
||||||
|
assert len(h) == 4, f"expected 4 heirs, got {len(h)}"
|
||||||
|
assert list(h.keys()) == ["aaaa", "lucia", "mario", "mario2"]
|
||||||
|
|
||||||
|
# Mock the Electrum-heavy parts so the build can run in a test context.
|
||||||
|
wallet = MagicMock()
|
||||||
|
wallet.dust_threshold.return_value = 500
|
||||||
|
wallet.get_change_addresses_for_new_transaction.return_value = [
|
||||||
|
"bcrt1q0000000000000000000000000000000000000"
|
||||||
|
]
|
||||||
|
|
||||||
|
bal_plugin = MagicMock()
|
||||||
|
bal_plugin.NO_WILLEXECUTOR.get.return_value = True # bypass will-executors
|
||||||
|
bal_plugin.get_decimal_point.return_value = 8
|
||||||
|
|
||||||
|
# Mock PartialTxOutput.from_address_and_value to return something usable.
|
||||||
|
_fake_outputs = []
|
||||||
|
|
||||||
|
def _fake_from_address_and_value(address, value):
|
||||||
|
out = MagicMock()
|
||||||
|
out.value = value
|
||||||
|
out.address = address
|
||||||
|
out.scriptpubkey = b""
|
||||||
|
out.script_descriptor = ""
|
||||||
|
_fake_outputs.append(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Mock PartialTransaction.from_io to record inputs and return a fake tx.
|
||||||
|
_fake_txs = []
|
||||||
|
|
||||||
|
def _fake_from_io(inputs, outputs, locktime, version):
|
||||||
|
tx = MagicMock()
|
||||||
|
tx.txid.return_value = f"faketx_{len(_fake_txs):04x}"
|
||||||
|
tx.estimated_size.return_value = 100
|
||||||
|
tx.get_fee.return_value = 100
|
||||||
|
tx.input_value.return_value = sum(inp.value_sats() for inp in inputs)
|
||||||
|
tx.output_value.return_value = sum(out.value for out in outputs)
|
||||||
|
tx.get_output_idxs_from_address.return_value = [0]
|
||||||
|
# Attributes set by the caller after construction.
|
||||||
|
tx.description = ""
|
||||||
|
tx.heirsvalue = 0
|
||||||
|
tx.my_locktime = 0
|
||||||
|
tx.willexecutor = None
|
||||||
|
tx.heirs = {}
|
||||||
|
tx.available_utxos = []
|
||||||
|
_fake_txs.append(tx)
|
||||||
|
return tx
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"bal.core.heirs.bitcoin.is_address", return_value=True
|
||||||
|
), patch(
|
||||||
|
"bal.core.heirs.PartialTxOutput.from_address_and_value",
|
||||||
|
side_effect=_fake_from_address_and_value,
|
||||||
|
), patch(
|
||||||
|
"bal.core.heirs.PartialTransaction.from_io",
|
||||||
|
side_effect=_fake_from_io,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
result = h.buildTransactions(
|
||||||
|
bal_plugin, wallet, tx_fees=1, utxos=utxos
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
pytest.fail(
|
||||||
|
f"buildTransactions raised {type(exc).__name__}: {exc}\n\n{tb}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result, (
|
||||||
|
"buildTransactions returned empty — no transactions were built "
|
||||||
|
"(check whether NO_WILLEXECUTOR needs to be True)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -519,4 +648,4 @@ if __name__ == "__main__":
|
|||||||
if name.startswith("test_"):
|
if name.startswith("test_"):
|
||||||
globals()[name]()
|
globals()[name]()
|
||||||
print(f" [OK] {name}")
|
print(f" [OK] {name}")
|
||||||
print("[OK] All Group E (giovanna7) tests passed")
|
print("[OK] All Group E (karen7) tests passed")
|
||||||
332
tests/test_reproduce_none_type.py
Normal file
332
tests/test_reproduce_none_type.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
"""
|
||||||
|
Reproduce the 'NoneType object has no attribute get' error that appears
|
||||||
|
in the GUI when the user deletes old will transactions and clicks Check.
|
||||||
|
|
||||||
|
This exercises the exact same code path as BalBuildWillDialog.task_phase1
|
||||||
|
but without requiring a full Qt event loop.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
|
# Electrum source
|
||||||
|
ELECTRUM_DIR = os.path.expanduser("~/devel/bal/electrum")
|
||||||
|
if os.path.isdir(ELECTRUM_DIR):
|
||||||
|
sys.path.insert(0, ELECTRUM_DIR)
|
||||||
|
|
||||||
|
from bal.core.heirs import Heirs
|
||||||
|
from bal.core.willexecutors import Willexecutors
|
||||||
|
from bal.core.will import Will, WillItem, NotCompleteWillException, NoHeirsException, NoWillExecutorNotPresent
|
||||||
|
from bal.core.plugin_base import BalPlugin, BalTimestamp
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Load karen7 wallet data
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
wallet_path = os.path.join(os.path.dirname(__file__), "karen7")
|
||||||
|
with open(wallet_path) as f:
|
||||||
|
KAREN7_DATA = json.load(f)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Minimal UTXO stub
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
class FakeUTXO:
|
||||||
|
def __init__(self, txid, out_idx, value_sats):
|
||||||
|
self._value = value_sats
|
||||||
|
self.prevout = MagicMock()
|
||||||
|
self.prevout.txid = txid
|
||||||
|
self.prevout.out_idx = out_idx
|
||||||
|
|
||||||
|
def value_sats(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def build_utxos(data):
|
||||||
|
utxos = []
|
||||||
|
txo = data.get("txo", {})
|
||||||
|
for txid, outputs in txo.items():
|
||||||
|
if not isinstance(outputs, dict):
|
||||||
|
continue
|
||||||
|
for addr, out_map in outputs.items():
|
||||||
|
if not isinstance(out_map, dict):
|
||||||
|
continue
|
||||||
|
for idx, info in out_map.items():
|
||||||
|
if not isinstance(info, list) or len(info) < 2:
|
||||||
|
continue
|
||||||
|
value, spent = info[0], info[1]
|
||||||
|
if spent is False:
|
||||||
|
utxos.append(FakeUTXO(txid, int(idx), value))
|
||||||
|
return utxos
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBalWindow:
|
||||||
|
"""Simulates bal_window for the dialog's task_phase1 flow."""
|
||||||
|
|
||||||
|
def __init__(self, heirs_obj, will_settings, bal_plugin, wallet,
|
||||||
|
window_wallet, willitems=None):
|
||||||
|
self.heirs = heirs_obj
|
||||||
|
self.will_settings = will_settings
|
||||||
|
self.bal_plugin = bal_plugin
|
||||||
|
self.wallet = wallet
|
||||||
|
self.window = MagicMock()
|
||||||
|
self.window.wallet = window_wallet
|
||||||
|
self.willitems = willitems or {}
|
||||||
|
self.will = {}
|
||||||
|
self.willexecutors = {}
|
||||||
|
self.no_willexecutor = None
|
||||||
|
self.date_to_check = None
|
||||||
|
|
||||||
|
def init_class_variables(self):
|
||||||
|
if not self.heirs:
|
||||||
|
raise NoHeirsException("Heirs are not defined")
|
||||||
|
from bal.core.plugin_base import BalTimestamp
|
||||||
|
from datetime import datetime
|
||||||
|
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
|
||||||
|
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()
|
||||||
|
self.willexecutors = Willexecutors.get_willexecutors(
|
||||||
|
self.bal_plugin, update=False, bal_window=self
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not self.bal_plugin.is_basic_mode()
|
||||||
|
and self.date_to_check < datetime.now().timestamp()
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_will(self):
|
||||||
|
return Will.is_will_valid(
|
||||||
|
self.willitems,
|
||||||
|
self.date_to_check,
|
||||||
|
self.will_settings["baltx_fees"],
|
||||||
|
self.window.wallet.get_utxos(),
|
||||||
|
heirs=self.heirs,
|
||||||
|
willexecutors=self.willexecutors,
|
||||||
|
self_willexecutor=self.no_willexecutor,
|
||||||
|
wallet=self.wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_will(self):
|
||||||
|
will = {}
|
||||||
|
self.willexecutors = Willexecutors.get_willexecutors(
|
||||||
|
self.bal_plugin, update=False, bal_window=self
|
||||||
|
)
|
||||||
|
if not self.no_willexecutor:
|
||||||
|
f = False
|
||||||
|
for _u, w in self.willexecutors.items():
|
||||||
|
if Willexecutors.is_selected(w):
|
||||||
|
f = True
|
||||||
|
if not f:
|
||||||
|
raise NoWillExecutorNotPresent(
|
||||||
|
"No Will-Executor or backup transaction selected"
|
||||||
|
)
|
||||||
|
txs = self.heirs.get_transactions(
|
||||||
|
self.bal_plugin,
|
||||||
|
self.window.wallet,
|
||||||
|
self.will_settings["baltx_fees"],
|
||||||
|
None,
|
||||||
|
self.date_to_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
creation_time = time.time()
|
||||||
|
if txs:
|
||||||
|
for txid in txs:
|
||||||
|
tx = {}
|
||||||
|
tx["tx"] = txs[txid]
|
||||||
|
tx["my_locktime"] = txs[txid].my_locktime
|
||||||
|
tx["heirsvalue"] = txs[txid].heirsvalue
|
||||||
|
tx["description"] = txs[txid].description
|
||||||
|
tx["willexecutor"] = copy.deepcopy(txs[txid].willexecutor)
|
||||||
|
tx["status"] = "New"
|
||||||
|
tx["baltx_fees"] = txs[txid].tx_fees
|
||||||
|
tx["time"] = creation_time
|
||||||
|
tx["heirs"] = copy.deepcopy(txs[txid].heirs)
|
||||||
|
tx["txchildren"] = []
|
||||||
|
will[txid] = WillItem(tx, _id=txid, wallet=self.wallet)
|
||||||
|
Will.update_will(self.willitems, will)
|
||||||
|
self.willitems.update(will)
|
||||||
|
Will.normalize_will(self.willitems, self.wallet)
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
return self.willitems
|
||||||
|
|
||||||
|
def update_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_simulate_task_phase1():
|
||||||
|
"""Simulate the exact steps that task_phase1 runs, catching any NoneType error."""
|
||||||
|
|
||||||
|
# 1. Build Heirs from karen7 data
|
||||||
|
heirs_data = KAREN7_DATA["heirs"]
|
||||||
|
h = Heirs.__new__(Heirs)
|
||||||
|
h.update(heirs_data)
|
||||||
|
assert len(h) == 4
|
||||||
|
|
||||||
|
# 2. Build UTXOs
|
||||||
|
utxos = build_utxos(KAREN7_DATA)
|
||||||
|
assert len(utxos) > 0, "no unspent UTXOs"
|
||||||
|
|
||||||
|
# 3. Create will_settings dict
|
||||||
|
default_settings = BalPlugin.default_will_settings()
|
||||||
|
will_settings = {
|
||||||
|
"baltx_fees": 1,
|
||||||
|
"threshold": default_settings["threshold"],
|
||||||
|
"locktime": default_settings["locktime"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Create mock wallet
|
||||||
|
wallet = MagicMock()
|
||||||
|
wallet.dust_threshold.return_value = 500
|
||||||
|
wallet.get_change_addresses_for_new_transaction.return_value = [
|
||||||
|
"bcrt1q0000000000000000000000000000000000000"
|
||||||
|
]
|
||||||
|
wallet.get_utxos.return_value = utxos
|
||||||
|
wallet.db = MagicMock()
|
||||||
|
wallet.db.get.return_value = heirs_data
|
||||||
|
|
||||||
|
window_wallet = MagicMock()
|
||||||
|
window_wallet.get_utxos.return_value = utxos
|
||||||
|
window_wallet.dust_threshold.return_value = 500
|
||||||
|
|
||||||
|
# 5. Create bal_plugin
|
||||||
|
bal_plugin = MagicMock()
|
||||||
|
bal_plugin.NO_WILLEXECUTOR.get.return_value = True
|
||||||
|
bal_plugin.ENABLE_MULTIVERSE.get.return_value = False
|
||||||
|
bal_plugin.WILL_SETTINGS.get.return_value = will_settings
|
||||||
|
bal_plugin.is_basic_mode.return_value = True
|
||||||
|
bal_plugin.get_decimal_point.return_value = 8
|
||||||
|
|
||||||
|
# 6. Create FakeBalWindow with EMPTY willitems (user deleted all transactions)
|
||||||
|
bw = FakeBalWindow(
|
||||||
|
heirs_obj=h,
|
||||||
|
will_settings=will_settings,
|
||||||
|
bal_plugin=bal_plugin,
|
||||||
|
wallet=wallet,
|
||||||
|
window_wallet=window_wallet,
|
||||||
|
willitems={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 7. Mock the Electrum-heavy parts for buildTransactions
|
||||||
|
_fake_outputs = []
|
||||||
|
def _fake_from_address_and_value(address, value):
|
||||||
|
out = MagicMock()
|
||||||
|
out.value = value
|
||||||
|
out.address = address
|
||||||
|
out.scriptpubkey = b""
|
||||||
|
out.script_descriptor = ""
|
||||||
|
_fake_outputs.append(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
_fake_txs = []
|
||||||
|
def _fake_from_io(inputs, outputs, locktime, version):
|
||||||
|
tx = MagicMock()
|
||||||
|
tx.txid.return_value = f"faketx_{len(_fake_txs):04x}"
|
||||||
|
tx.estimated_size.return_value = 100
|
||||||
|
tx.get_fee.return_value = 100
|
||||||
|
tx.input_value.return_value = sum(inp.value_sats() for inp in inputs)
|
||||||
|
tx.output_value.return_value = sum(out.value for out in outputs)
|
||||||
|
tx.get_output_idxs_from_address.return_value = [0]
|
||||||
|
tx.description = ""
|
||||||
|
tx.heirsvalue = 0
|
||||||
|
tx.my_locktime = 0
|
||||||
|
tx.willexecutor = None
|
||||||
|
tx.heirs = {}
|
||||||
|
tx.available_utxos = []
|
||||||
|
tx.tx_fees = 1
|
||||||
|
_fake_txs.append(tx)
|
||||||
|
return tx
|
||||||
|
|
||||||
|
patches = [
|
||||||
|
patch("bal.core.heirs.bitcoin.is_address", return_value=True),
|
||||||
|
patch("bal.core.heirs.PartialTxOutput.from_address_and_value",
|
||||||
|
side_effect=_fake_from_address_and_value),
|
||||||
|
patch("bal.core.heirs.PartialTransaction.from_io",
|
||||||
|
side_effect=_fake_from_io),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Monkey-patch Will.get_tx_from_any to accept MagicMock
|
||||||
|
original_get_tx_from_any = Will.get_tx_from_any
|
||||||
|
def patched_get_tx_from_any(a):
|
||||||
|
if isinstance(a, MagicMock):
|
||||||
|
return a
|
||||||
|
return original_get_tx_from_any(a)
|
||||||
|
Will.get_tx_from_any = staticmethod(patched_get_tx_from_any)
|
||||||
|
|
||||||
|
with contextlib.ExitStack() as stack:
|
||||||
|
for p in patches:
|
||||||
|
stack.enter_context(p)
|
||||||
|
|
||||||
|
# STEP A: init_class_variables
|
||||||
|
try:
|
||||||
|
bw.init_class_variables()
|
||||||
|
print("[OK] init_class_variables")
|
||||||
|
except Exception as e:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
print(f"[FAIL] init_class_variables raised {type(e).__name__}: {e}")
|
||||||
|
print(tb)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# STEP B: check_amounts (expect PercAmountException for 101%)
|
||||||
|
try:
|
||||||
|
Will.check_amounts(
|
||||||
|
bw.heirs,
|
||||||
|
bw.willexecutors,
|
||||||
|
bw.window.wallet.get_utxos(),
|
||||||
|
bw.date_to_check,
|
||||||
|
bw.window.wallet.dust_threshold(),
|
||||||
|
)
|
||||||
|
print("[OK] check_amounts")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[NOTE] check_amounts raised {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
# STEP C: check_will (empty willitems -> NotCompleteWillException)
|
||||||
|
have_to_build = False
|
||||||
|
try:
|
||||||
|
bw.check_will()
|
||||||
|
print("[OK] check_will")
|
||||||
|
except NotCompleteWillException:
|
||||||
|
have_to_build = True
|
||||||
|
print("[OK] check_will: NotCompleteWillException (expected for empty will)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FAIL] check_will raised unexpected {type(e).__name__}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
# STEP D: build_will
|
||||||
|
if have_to_build:
|
||||||
|
try:
|
||||||
|
txs = bw.build_will()
|
||||||
|
if txs:
|
||||||
|
print(f"[OK] build_will: built {len(txs)} transaction(s)")
|
||||||
|
# Now simulate the post-build steps from task_phase1
|
||||||
|
for wid in Will.only_valid(bw.willitems):
|
||||||
|
heirs = bw.willitems[wid].heirs
|
||||||
|
print(f" - will txid: {wid[:16]}..., heirs: {list(heirs.keys())}")
|
||||||
|
else:
|
||||||
|
print("[NOTE] build_will returned empty")
|
||||||
|
except Exception as e:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
print(f"[FAIL] build_will raised {type(e).__name__}: {e}")
|
||||||
|
print(tb)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
print("[SKIP] build_will")
|
||||||
|
|
||||||
|
# STEP E: Simulate what check() does after dialog returns
|
||||||
|
print("\n--- Post-dialog steps ---")
|
||||||
|
will_to_check = {}
|
||||||
|
for wid, w in bw.willitems.items():
|
||||||
|
if Will.needs_server_check(w):
|
||||||
|
will_to_check[wid] = w
|
||||||
|
print(f"[OK] needs_server_check: {len(will_to_check)} need checking")
|
||||||
|
|
||||||
|
print("\n[DONE] All steps completed without NoneType error")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_simulate_task_phase1()
|
||||||
Reference in New Issue
Block a user