BalWindow/plugin: history persistence, will import/merge, sig tracking, local-spender fixes
This commit is contained in:
559
tests/test_import_will_details.py
Normal file
559
tests/test_import_will_details.py
Normal file
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
Tests for the "Import" (read-only will preview) flow.
|
||||
|
||||
Covers:
|
||||
- ``BalWindow._load_will_file`` round-trip (file -> WillItems).
|
||||
- ``import_will_into_details`` normalization + IMPORTED status.
|
||||
- ``BalWindow.sign_transactions`` operating ONLY on the passed (imported)
|
||||
will, never on the live wallet state.
|
||||
- ``WillWidget`` honouring an explicit ``will`` argument.
|
||||
- ``WillDetailDialog`` external-will mode (threshold + isolated buttons).
|
||||
|
||||
Run:
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/test_import_will_details.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from types import MethodType, SimpleNamespace
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bal.core.will import Will, WillItem
|
||||
from bal.gui.qt import window as window_mod
|
||||
|
||||
_app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, version 2)
|
||||
_VALID_TX_HEX = (
|
||||
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
|
||||
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
|
||||
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
|
||||
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
|
||||
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
|
||||
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
|
||||
"42146f11ef8414ae929feaafc388ac00000000"
|
||||
)
|
||||
|
||||
|
||||
def _make_willitem_dict(**overrides):
|
||||
"""Return a minimal dict that can construct a WillItem."""
|
||||
d = {
|
||||
"tx": _VALID_TX_HEX,
|
||||
"heirs": {},
|
||||
"willexecutor": None,
|
||||
"status": "",
|
||||
"description": "",
|
||||
"time": 0,
|
||||
"change": "",
|
||||
"baltx_fees": 100,
|
||||
}
|
||||
d.update(overrides)
|
||||
return d
|
||||
|
||||
|
||||
def _make_willitems(n, prefix="w"):
|
||||
"""Create ``n`` WillItems with distinct txids/locktimes."""
|
||||
willitems = {}
|
||||
for i in range(n):
|
||||
wid = f"{prefix}{i}"
|
||||
wi = WillItem(_make_willitem_dict())
|
||||
wi.tx.locktime = 1000 + i
|
||||
wi._id = wid
|
||||
willitems[wid] = wi
|
||||
return willitems
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# _load_will_file round-trip
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_load_will_file_roundtrip():
|
||||
from bal.gui.qt.common import write_json_file
|
||||
|
||||
src = _make_willitems(2)
|
||||
data = {wid: wi.to_dict() for wid, wi in src.items()}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False
|
||||
) as f:
|
||||
path = f.name
|
||||
write_json_file(path, data)
|
||||
try:
|
||||
loaded = window_mod.BalWindow._load_will_file(None, path)
|
||||
assert set(loaded) == {"w0", "w1"}
|
||||
for wid, wi in loaded.items():
|
||||
assert isinstance(wi, WillItem)
|
||||
assert wi.heirs == {}
|
||||
assert wi._id == wid
|
||||
assert wi.tx is not None
|
||||
finally:
|
||||
import os
|
||||
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# import_will_into_details normalization + IMPORTED status
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_imported_will_is_normalized_and_marked_imported():
|
||||
willitems = _make_willitems(1)
|
||||
Will.normalize_will(willitems, None)
|
||||
for wi in willitems.values():
|
||||
wi.set_status("IMPORTED", True)
|
||||
assert all(wi.get_status("IMPORTED") for wi in willitems.values())
|
||||
assert all(wi.get_status("VALID") for wi in willitems.values())
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# sign_transactions operates only on the passed (imported) will
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_sign_transactions_external_only():
|
||||
class FakeWallet:
|
||||
def sign_transaction(self, tx, password, ignore_warnings=False):
|
||||
# No-op: never marks the tx complete.
|
||||
return None
|
||||
|
||||
live = _make_willitems(1, prefix="live")
|
||||
wid_live = next(iter(live))
|
||||
imported = _make_willitems(2, prefix="imp")
|
||||
|
||||
fake = SimpleNamespace(
|
||||
willitems=live,
|
||||
wallet=FakeWallet(),
|
||||
waiting_dialog=SimpleNamespace(update=lambda msg: None),
|
||||
)
|
||||
|
||||
result = window_mod.BalWindow.sign_transactions(fake, None, will=imported)
|
||||
|
||||
assert result is not None
|
||||
assert set(result) == set(imported)
|
||||
assert wid_live not in result
|
||||
# The live will must be completely untouched by the external sign run.
|
||||
assert live[wid_live].get_status("COMPLETE") is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# WillWidget honours an explicit ``will`` argument
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_will_widget_explicit_will():
|
||||
from bal.gui.qt.widgets import WillWidget
|
||||
|
||||
live = _make_willitems(1, prefix="live")
|
||||
imported = _make_willitems(2, prefix="imp")
|
||||
|
||||
fake_parent = SimpleNamespace(
|
||||
decimal_point=8,
|
||||
base_unit_name="BTC",
|
||||
bal_window=SimpleNamespace(
|
||||
willitems=live,
|
||||
bal_plugin=SimpleNamespace(
|
||||
_hide_replaced=False, _hide_invalidated=False
|
||||
),
|
||||
show_transaction=lambda *a, **k: None,
|
||||
),
|
||||
)
|
||||
|
||||
w = WillWidget(parent=fake_parent, will=imported)
|
||||
assert w.will is imported
|
||||
|
||||
w2 = WillWidget(parent=fake_parent)
|
||||
assert w2.will is live
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# WillDetailDialog external-will mode
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _make_fake_bal_window(window_widget):
|
||||
bal_plugin = SimpleNamespace(read_file=lambda path: b"")
|
||||
return SimpleNamespace(
|
||||
window=window_widget,
|
||||
bal_plugin=bal_plugin,
|
||||
wallet=SimpleNamespace(),
|
||||
show_transaction=lambda *a, **k: None,
|
||||
willitems=_make_willitems(1),
|
||||
will_settings={"real_threshold": 9999},
|
||||
)
|
||||
|
||||
|
||||
def test_will_detail_dialog_external_threshold():
|
||||
from bal.gui.qt.dialogs import WillDetailDialog
|
||||
|
||||
window_widget = QWidget()
|
||||
bal_window = _make_fake_bal_window(window_widget)
|
||||
bal_window.window.config = SimpleNamespace()
|
||||
bal_window.window.format_amount = lambda *a, **k: "1.0"
|
||||
bal_window.window.base_unit = "BTC"
|
||||
bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0"
|
||||
bal_window.window.fx = None
|
||||
bal_window.window.format_fee_rate = lambda *a, **k: "1.0"
|
||||
bal_window.window.get_decimal_point = lambda: 8
|
||||
|
||||
imported = _make_willitems(2)
|
||||
# locktimes 1000 and 1001 -> threshold must be the max (1001).
|
||||
dialog = WillDetailDialog(bal_window, will=imported)
|
||||
|
||||
assert dialog._external_will is True
|
||||
assert dialog.will is imported
|
||||
assert dialog.threshold == 1001
|
||||
|
||||
dialog2 = WillDetailDialog(bal_window)
|
||||
assert dialog2._external_will is False
|
||||
assert dialog2.will is bal_window.willitems
|
||||
assert dialog2.threshold == 9999
|
||||
|
||||
|
||||
def test_will_detail_dialog_buttons_pass_will():
|
||||
from bal.gui.qt.dialogs import WillDetailDialog
|
||||
|
||||
window_widget = QWidget()
|
||||
bal_window = _make_fake_bal_window(window_widget)
|
||||
bal_window.window.config = SimpleNamespace()
|
||||
bal_window.window.format_amount = lambda *a, **k: "1.0"
|
||||
bal_window.window.base_unit = "BTC"
|
||||
bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0"
|
||||
bal_window.window.fx = None
|
||||
bal_window.window.format_fee_rate = lambda *a, **k: "1.0"
|
||||
bal_window.window.get_decimal_point = lambda: 8
|
||||
|
||||
imported = _make_willitems(1)
|
||||
dialog = WillDetailDialog(bal_window, will=imported)
|
||||
|
||||
calls = []
|
||||
|
||||
bal_window.ask_password_and_sign_transactions = lambda **k: calls.append(
|
||||
("sign", k.get("will"))
|
||||
)
|
||||
bal_window.broadcast_transactions = lambda **k: calls.append(
|
||||
("broadcast", k.get("will"))
|
||||
)
|
||||
bal_window.export_will = lambda **k: calls.append(("export", k.get("will")))
|
||||
bal_window.invalidate_will = lambda **k: calls.append(
|
||||
("invalidate", k.get("will"))
|
||||
)
|
||||
|
||||
dialog.ask_password_and_sign_transactions()
|
||||
dialog.broadcast_transactions()
|
||||
dialog.export_will()
|
||||
dialog.invalidate_will()
|
||||
|
||||
assert len(calls) == 4
|
||||
for action, will in calls:
|
||||
assert will is imported, f"{action} did not receive the imported will"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Merge flow (BalWindow.merge_will)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _make_partial_tx(locktime=1000, signed=False):
|
||||
"""A PartialTransaction derived from _VALID_TX_HEX.
|
||||
|
||||
When ``signed`` the input scriptSig of the raw tx is copied over, which
|
||||
finalizes the (legacy P2PKH) input and makes ``is_complete()`` True.
|
||||
"""
|
||||
from electrum.transaction import PartialTransaction, Transaction
|
||||
|
||||
ptx = PartialTransaction.from_tx(Transaction(_VALID_TX_HEX))
|
||||
ptx.locktime = locktime
|
||||
if signed:
|
||||
raw = Transaction(_VALID_TX_HEX)
|
||||
ptx.inputs()[0].script_sig = raw.inputs()[0].script_sig
|
||||
return ptx
|
||||
|
||||
|
||||
def _make_willitem_with_tx(tx, key=None):
|
||||
wi = WillItem(_make_willitem_dict())
|
||||
wi.tx = tx
|
||||
wi._id = key if key is not None else tx.txid()
|
||||
return wi
|
||||
|
||||
|
||||
def _make_merge_fake(willitems):
|
||||
"""A BalWindow-like object with a wallet stub sufficient for the local
|
||||
validity check that ``merge_will`` runs after merging.
|
||||
"""
|
||||
class FakeWallet:
|
||||
def add_input_info(self, txin, **kwargs):
|
||||
pass
|
||||
|
||||
def add_output_info(self, txout, **kwargs):
|
||||
pass
|
||||
|
||||
def get_utxos(self):
|
||||
return []
|
||||
|
||||
def get_tx_info(self, tx):
|
||||
return SimpleNamespace(
|
||||
tx_mined_status=SimpleNamespace(height=lambda: 0)
|
||||
)
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return SimpleNamespace(get_transaction=lambda txid: None)
|
||||
|
||||
calls = []
|
||||
fake = SimpleNamespace(
|
||||
willitems=willitems,
|
||||
will={},
|
||||
wallet=FakeWallet(),
|
||||
bal_window=None,
|
||||
date_to_check=1700000000,
|
||||
bal_plugin=SimpleNamespace(
|
||||
HISTORY_LABEL=SimpleNamespace(
|
||||
get=lambda: "BAL will history ({willexecutor})"
|
||||
)
|
||||
),
|
||||
update_all=lambda: calls.append("update_all"),
|
||||
)
|
||||
fake.save_willitems = MethodType(window_mod.BalWindow.save_willitems, fake)
|
||||
return fake, calls
|
||||
|
||||
|
||||
def test_merge_will_same_id_unsigned_live_substitutes_signed_imported():
|
||||
# The realistic "signed on another machine, imported here" case: the live
|
||||
# will holds an unsigned PSBT (txid() -> None), the imported one is signed
|
||||
# (complete). The transaction must be substituted and COMPLETE set.
|
||||
wid = "same_id"
|
||||
live_tx = _make_partial_tx(locktime=1000, signed=False)
|
||||
imported_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
|
||||
live = {wid: _make_willitem_with_tx(live_tx, key=wid)}
|
||||
imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)}
|
||||
imported[wid].set_status("COMPLETE", True)
|
||||
imported[wid].set_status("PUSHED", True)
|
||||
imported[wid].set_status("CHECKED", True)
|
||||
|
||||
fake, calls = _make_merge_fake(live)
|
||||
live_item = live[wid]
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
# The live WillItem object is kept (never replaced) and the signed tx
|
||||
# substituted in.
|
||||
assert live[wid] is live_item
|
||||
assert live_item.tx is imported_tx
|
||||
assert live_item.tx.is_complete()
|
||||
assert live_item.get_status("COMPLETE") is True
|
||||
# Operational statuses were carried over.
|
||||
assert live_item.get_status("PUSHED") is True
|
||||
assert live_item.get_status("CHECKED") is True
|
||||
# The will was saved and the GUI refreshed.
|
||||
assert wid in fake.will
|
||||
assert fake.will[wid]["COMPLETE"] is True
|
||||
assert "update_all" in calls
|
||||
|
||||
|
||||
def test_merge_will_same_id_both_signed_combines():
|
||||
# Live tx is signed but the COMPLETE flag was not set yet; the imported
|
||||
# signed tx with the same txid must be COMBINED into the live one (the live
|
||||
# tx object is kept) rather than substituted.
|
||||
from electrum.transaction import Transaction
|
||||
|
||||
wid = "same_id_combine"
|
||||
imported_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
txid = imported_tx.txid()
|
||||
assert txid is not None
|
||||
|
||||
live_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
live = {wid: _make_willitem_with_tx(live_tx, key=wid)}
|
||||
imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)}
|
||||
imported[wid].set_status("COMPLETE", True)
|
||||
|
||||
fake, _ = _make_merge_fake(live)
|
||||
live_item = live[wid]
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
# Same txid -> combine_with_other_psbt: live tx object is preserved.
|
||||
assert live_item.tx is live_tx
|
||||
assert live_item.tx.is_complete()
|
||||
assert live_item.get_status("COMPLETE") is True
|
||||
raw_sig = Transaction(_VALID_TX_HEX).inputs()[0].script_sig
|
||||
assert live_item.tx.inputs()[0].script_sig == raw_sig
|
||||
|
||||
|
||||
def test_merge_will_same_id_already_complete_never_touches_live_tx():
|
||||
wid = "already_complete"
|
||||
live_tx = _make_partial_tx(locktime=1000, signed=False)
|
||||
live = {wid: _make_willitem_with_tx(live_tx, key=wid)}
|
||||
live[wid].set_status("COMPLETE", True)
|
||||
|
||||
imported_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)}
|
||||
imported[wid].set_status("COMPLETE", True)
|
||||
imported[wid].set_status("PUSHED", True)
|
||||
|
||||
fake, _ = _make_merge_fake(live)
|
||||
live_item = live[wid]
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
# An already-signed live will is left untouched: no combine, no substitute.
|
||||
assert live_item.tx is live_tx
|
||||
assert live_item.tx.is_complete() is False
|
||||
assert live_item.tx.inputs()[0].script_sig is None
|
||||
assert live_item.get_status("COMPLETE") is True
|
||||
assert live_item.get_status("PUSHED") is True
|
||||
|
||||
|
||||
def test_merge_will_statuses_are_monotonic():
|
||||
# Statuses that are True in the live item must never be cleared by a False
|
||||
# value coming from the imported item.
|
||||
wid = "monotonic"
|
||||
live_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
live = {wid: _make_willitem_with_tx(live_tx, key=wid)}
|
||||
live[wid].set_status("CONFIRMED", True)
|
||||
|
||||
imported_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)}
|
||||
imported[wid].set_status("MEMPOOL", True)
|
||||
|
||||
fake, _ = _make_merge_fake(live)
|
||||
live_item = live[wid]
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
assert live_item.get_status("CONFIRMED") is True
|
||||
assert live_item.get_status("MEMPOOL") is True
|
||||
assert live_item.get_status("COMPLETE") is True
|
||||
|
||||
|
||||
def test_merge_will_new_ids_added_wholesale():
|
||||
live_tx = _make_partial_tx(locktime=1000, signed=True)
|
||||
live = {live_tx.txid(): _make_willitem_with_tx(live_tx)}
|
||||
|
||||
imported_tx = _make_partial_tx(locktime=2000, signed=True)
|
||||
new_id = imported_tx.txid()
|
||||
imported = {new_id: _make_willitem_with_tx(imported_tx)}
|
||||
imported[new_id].set_status("PUSHED", True)
|
||||
|
||||
fake, _ = _make_merge_fake(live)
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
assert len(live) == 2
|
||||
assert live[new_id] is imported[new_id]
|
||||
assert live[new_id].get_status("PUSHED") is True
|
||||
assert new_id in fake.will
|
||||
|
||||
|
||||
def test_merge_will_from_file_invalid_file_raises():
|
||||
from bal.gui.qt.common import FileImportFailed
|
||||
|
||||
def bad_load(path):
|
||||
raise ValueError("bad file")
|
||||
|
||||
fake = SimpleNamespace(_load_will_file=bad_load, wallet=None)
|
||||
|
||||
try:
|
||||
window_mod.BalWindow.merge_will_from_file(fake, "/nonexistent.json")
|
||||
except FileImportFailed as e:
|
||||
assert "bad file" in str(e)
|
||||
else:
|
||||
raise AssertionError("expected FileImportFailed")
|
||||
|
||||
|
||||
def test_merge_will_partial_signatures_update_counts():
|
||||
# A live unsigned 2-of-3 multisig will merged with an imported copy that
|
||||
# carries 1 signature must end up PARTIALLY_SIGNED with the sig counts
|
||||
# refreshed on the live item (check_signatures runs after the merge).
|
||||
from binascii import unhexlify
|
||||
|
||||
from electrum import crypto
|
||||
from electrum.descriptor import parse_descriptor
|
||||
from electrum.transaction import (
|
||||
PartialTransaction,
|
||||
PartialTxInput,
|
||||
PartialTxOutput,
|
||||
Sighash,
|
||||
TxOutpoint,
|
||||
)
|
||||
|
||||
def make_multisig_tx(nsigs):
|
||||
pubs = []
|
||||
for seed in (1, 2, 3):
|
||||
pubs.append(crypto.privkey_to_pubkey(bytes([seed] * 32)).hex())
|
||||
desc = parse_descriptor("wsh(multi(2,{}))".format(",".join(pubs)))
|
||||
txin = PartialTxInput(prevout=TxOutpoint(b"\x11" * 32, 0), script_sig=b"")
|
||||
txin.script_descriptor = desc
|
||||
txin._trusted_value_sats = 100000
|
||||
txin.sighash = Sighash.ALL
|
||||
sig = b"\x30\x44\x02\x20" + b"\x01" * 32 + b"\x02\x20" + b"\x02" * 32
|
||||
for i in range(nsigs):
|
||||
txin.sigs_ecdsa[unhexlify(pubs[i])] = sig
|
||||
from electrum.bitcoin import public_key_to_p2wpkh
|
||||
|
||||
addr = public_key_to_p2wpkh(bytes.fromhex(pubs[0]))
|
||||
ptx = PartialTransaction()
|
||||
ptx.add_inputs([txin])
|
||||
ptx.add_outputs([PartialTxOutput.from_address_and_value(addr, 50000)])
|
||||
ptx.locktime = 1000
|
||||
return ptx, desc
|
||||
|
||||
wid = "multisig_will"
|
||||
live_tx, _ = make_multisig_tx(0)
|
||||
imported_tx, _ = make_multisig_tx(1)
|
||||
|
||||
live = {wid: _make_willitem_with_tx(live_tx, key=wid)}
|
||||
imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)}
|
||||
|
||||
fake, _ = _make_merge_fake(live)
|
||||
live_item = live[wid]
|
||||
|
||||
window_mod.BalWindow.merge_will(fake, imported)
|
||||
|
||||
assert live_item.get_status("PARTIALLY_SIGNED") is True
|
||||
assert live_item.sigs_have == 1
|
||||
assert live_item.sigs_required == 2
|
||||
|
||||
|
||||
def test_will_detail_dialog_merge_switches_to_live():
|
||||
from bal.gui.qt.dialogs import WillDetailDialog
|
||||
|
||||
window_widget = QWidget()
|
||||
bal_window = _make_fake_bal_window(window_widget)
|
||||
bal_window.window.config = SimpleNamespace()
|
||||
bal_window.window.format_amount = lambda *a, **k: "1.0"
|
||||
bal_window.window.base_unit = "BTC"
|
||||
bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0"
|
||||
bal_window.window.fx = None
|
||||
bal_window.window.format_fee_rate = lambda *a, **k: "1.0"
|
||||
bal_window.window.get_decimal_point = lambda: 8
|
||||
|
||||
live = bal_window.willitems
|
||||
imported = _make_willitems(2)
|
||||
merged = []
|
||||
bal_window.merge_will = lambda will: merged.append(will)
|
||||
|
||||
dialog = WillDetailDialog(bal_window, will=imported)
|
||||
|
||||
assert dialog._external_will is True
|
||||
assert dialog.merge_button is not None
|
||||
assert dialog.merge_button.isHidden() is False
|
||||
|
||||
dialog.merge_will()
|
||||
|
||||
assert merged == [imported]
|
||||
assert dialog._external_will is False
|
||||
assert dialog.will is live
|
||||
assert dialog.threshold == bal_window.will_settings["real_threshold"]
|
||||
assert dialog.merge_button.isHidden() is True
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Main
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All import-will-details tests passed")
|
||||
Reference in New Issue
Block a user