core: extract GUI-free logic into bal.core (reminders/checkalive/input_rules); fix save/sign tx RLock pickle by serializing tx; keep invalid heirs in wallet on build; tb1 testnet will-executor addresses; move tests to core modules
This commit is contained in:
2209
tests/karen7
2209
tests/karen7
File diff suppressed because it is too large
Load Diff
95
tests/test_core_checkalive.py
Normal file
95
tests/test_core_checkalive.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Tests for ``bal.core.checkalive`` (pure, GUI-free).
|
||||
|
||||
Covers the CheckAliveError exception and the BASIC/ADVANCED date_to_check
|
||||
policy (``resolve_date_to_check`` / ``check_alive_expired``).
|
||||
|
||||
Run:
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
python3 tests/test_core_checkalive.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.core.checkalive import ( # noqa: E402 (path insert above)
|
||||
CheckAliveError,
|
||||
check_alive_expired,
|
||||
resolve_date_to_check,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# CheckAliveError
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_check_alive_error_default():
|
||||
err = CheckAliveError(1000000)
|
||||
assert err.timestamp_to_check == 1000000
|
||||
|
||||
|
||||
def test_check_alive_error_str():
|
||||
err = CheckAliveError(1000000)
|
||||
s = str(err)
|
||||
assert "Check alive expired" in s
|
||||
assert "1970" in s
|
||||
|
||||
|
||||
def test_check_alive_error_subclass():
|
||||
assert issubclass(CheckAliveError, Exception)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# resolve_date_to_check
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_basic_mode_uses_now():
|
||||
fake_now = 1_800_000_000.0
|
||||
assert resolve_date_to_check(True, {}, now=fake_now) == fake_now
|
||||
|
||||
|
||||
def test_advanced_mode_uses_threshold_absolute():
|
||||
threshold = time.time() + 5 * 86400
|
||||
settings = {"threshold": threshold}
|
||||
result = resolve_date_to_check(False, settings)
|
||||
assert abs(result - threshold) < 1
|
||||
|
||||
|
||||
def test_advanced_mode_parses_relative_threshold():
|
||||
# "30d" resolves to a future timestamp (midnight-normalised).
|
||||
settings = {"threshold": "30d"}
|
||||
result = resolve_date_to_check(False, settings)
|
||||
assert result > time.time()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# check_alive_expired
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_basic_mode_never_expired():
|
||||
assert check_alive_expired(True, 1_000_000_000.0) is False
|
||||
assert check_alive_expired(True, time.time() - 10_000) is False
|
||||
|
||||
|
||||
def test_advanced_expired_when_past():
|
||||
assert check_alive_expired(False, time.time() - 10_000) is True
|
||||
|
||||
|
||||
def test_advanced_not_expired_when_future():
|
||||
assert check_alive_expired(False, time.time() + 10_000) is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Main
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All core checkalive tests passed")
|
||||
165
tests/test_core_input_rules.py
Normal file
165
tests/test_core_input_rules.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Tests for ``bal.core.input_rules`` (pure, GUI-free).
|
||||
|
||||
Covers the locktime acceptance bounds, the RAW locktime sanitisation, and the
|
||||
percentage-or-amount field normalisation that the Qt editors wrap.
|
||||
|
||||
Run:
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
python3 tests/test_core_input_rules.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.core.input_rules import ( # noqa: E402 (path insert above)
|
||||
LockTimeEditor,
|
||||
normalize_locktime_raw_text,
|
||||
normalize_perc_amount_text,
|
||||
parse_perc_amount,
|
||||
replace_dy_suffixes,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# LockTimeEditor
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_locktime_editor_is_acceptable():
|
||||
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():
|
||||
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():
|
||||
assert LockTimeEditor.min_allowed_value >= 0
|
||||
assert LockTimeEditor.max_allowed_value > LockTimeEditor.min_allowed_value
|
||||
|
||||
|
||||
def test_locktime_editor_get_max_allowed_timestamp():
|
||||
# Always a valid, fromtimestamp-able value (the Windows 2038 clamp).
|
||||
ts = LockTimeEditor.get_max_allowed_timestamp()
|
||||
import datetime
|
||||
|
||||
datetime.datetime.fromtimestamp(ts) # must not raise
|
||||
assert ts <= 2**32 - 1
|
||||
|
||||
|
||||
def test_locktime_editor_subclass_bounds():
|
||||
class Tight(LockTimeEditor):
|
||||
min_allowed_value = 1000
|
||||
max_allowed_value = 2000
|
||||
|
||||
assert Tight.is_acceptable_locktime(1500) is True
|
||||
assert Tight.is_acceptable_locktime(999) is False
|
||||
assert Tight.is_acceptable_locktime(2001) is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# replace_dy_suffixes / RAW locktime sanitisation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_replace_dy_suffixes():
|
||||
# replace_str only strips the day ("d") and year ("y") suffixes. The
|
||||
# block-height suffix ("b") was removed (A1), so "b" is NOT stripped
|
||||
# anymore (locktimes are always UNIX timestamps now).
|
||||
assert replace_dy_suffixes("123d") == "123"
|
||||
assert replace_dy_suffixes("456y") == "456"
|
||||
# "b" is left untouched (no longer a recognised suffix)
|
||||
assert replace_dy_suffixes("789b") == "789b"
|
||||
# only d/y are stripped; a stray "b" remains
|
||||
assert replace_dy_suffixes("12d34y56b") == "123456b"
|
||||
|
||||
|
||||
def test_normalize_locktime_raw_empty():
|
||||
s, isdays, isyears, pos = normalize_locktime_raw_text("", 0)
|
||||
assert s == ""
|
||||
assert isdays is False
|
||||
assert isyears is False
|
||||
|
||||
|
||||
def test_normalize_locktime_raw_days():
|
||||
s, isdays, isyears, pos = normalize_locktime_raw_text("30d", 3)
|
||||
assert s == "30d"
|
||||
assert isdays is True
|
||||
assert isyears is False
|
||||
|
||||
|
||||
def test_normalize_locktime_raw_years():
|
||||
s, isdays, isyears, pos = normalize_locktime_raw_text("2y", 2)
|
||||
assert s == "2y"
|
||||
assert isdays is False
|
||||
assert isyears is True
|
||||
|
||||
|
||||
def test_normalize_locktime_raw_strips_bad_chars():
|
||||
s, isdays, isyears, pos = normalize_locktime_raw_text("12a34b56", 8)
|
||||
assert s == "123456"
|
||||
assert isdays is False
|
||||
assert isyears is False
|
||||
|
||||
|
||||
def test_normalize_locktime_raw_single_suffix():
|
||||
# "1y30d" collapses to a single suffix; "d" wins (processed first), so the
|
||||
# "y" is dropped along with all letters.
|
||||
s, isdays, isyears, pos = normalize_locktime_raw_text("1y30d", 5)
|
||||
assert isdays is True
|
||||
assert isyears is False
|
||||
assert s == "130d"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Percentage-or-amount normalisation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_perc_normalize_percent():
|
||||
s, is_perc = normalize_perc_amount_text("50%", ".")
|
||||
assert is_perc is True
|
||||
assert s == "50%"
|
||||
|
||||
|
||||
def test_perc_normalize_no_percent():
|
||||
s, is_perc = normalize_perc_amount_text("123", ".")
|
||||
assert is_perc is False
|
||||
assert s == "123"
|
||||
|
||||
|
||||
def test_perc_normalize_strips_invalid():
|
||||
s, is_perc = normalize_perc_amount_text("1a2b3", ".")
|
||||
assert s == "123"
|
||||
|
||||
|
||||
def test_perc_normalize_decimal_limit():
|
||||
# At most 8 decimals after the point.
|
||||
s, is_perc = normalize_perc_amount_text("1.1234567890123", ".")
|
||||
assert s == "1.12345678"
|
||||
|
||||
|
||||
def test_perc_parse():
|
||||
from decimal import Decimal
|
||||
|
||||
assert parse_perc_amount("50%", ".") == Decimal(50)
|
||||
assert parse_perc_amount("123.45", ".") == Decimal("123.45")
|
||||
assert parse_perc_amount("abc", ".") is None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Main
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All core input_rules tests passed")
|
||||
299
tests/test_core_reminders.py
Normal file
299
tests/test_core_reminders.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Tests for ``bal.core.reminders`` (pure, GUI-free).
|
||||
|
||||
Covers the reminder-offset rules (BASIC + ADVANCED), the RFC-5545 helpers
|
||||
(format_time, ical_escape, fold_ical_line), write_temp_ics, and the unified
|
||||
``build_ics_reminders`` builder.
|
||||
|
||||
This module imports only ``bal.core``, so it runs in the lint venv (no
|
||||
Electrum/Qt needed) as well as the runtime venv:
|
||||
|
||||
Run:
|
||||
python3 tests/test_core_reminders.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.core.reminders import ( # noqa: E402 (path insert above)
|
||||
BASIC_REMINDER_OFFSETS,
|
||||
basic_reminder_offsets,
|
||||
build_ics_reminders,
|
||||
compute_reminder_offsets,
|
||||
fold_ical_line,
|
||||
format_time,
|
||||
ical_escape,
|
||||
write_temp_ics,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# compute_reminder_offsets - distribution rules
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_offsets_long_period_three_reminders():
|
||||
"""30-day period, 3 reminders: spread out, all before the deadline."""
|
||||
offsets = compute_reminder_offsets(30, 3)
|
||||
assert len(offsets) == 3
|
||||
# All strictly before the deadline (offset >= 1 means "n days before end").
|
||||
assert all(o >= 1 for o in offsets)
|
||||
# Sorted earliest-first (largest offset first).
|
||||
assert offsets == sorted(offsets, reverse=True)
|
||||
# One reminder near the start, one near the end.
|
||||
assert max(offsets) == 30
|
||||
assert min(offsets) == 1
|
||||
|
||||
|
||||
def test_offsets_capped_at_one_per_day():
|
||||
"""Short period: at most one reminder per available day."""
|
||||
# 2 days but 3 requested -> only 2 reminders, one per day.
|
||||
assert compute_reminder_offsets(2, 3) == [2, 1]
|
||||
# 1 day but 3 requested -> a single reminder, the day before the deadline.
|
||||
assert compute_reminder_offsets(1, 3) == [1]
|
||||
|
||||
|
||||
def test_offsets_empty_when_no_room():
|
||||
"""No reminders when there is no day before the deadline."""
|
||||
assert compute_reminder_offsets(0, 3) == []
|
||||
assert compute_reminder_offsets(-5, 3) == []
|
||||
# A non-positive count also yields nothing.
|
||||
assert compute_reminder_offsets(30, 0) == []
|
||||
|
||||
|
||||
def test_offsets_never_exceed_requested_count():
|
||||
"""The number of reminders never exceeds the requested count (max 5)."""
|
||||
offsets = compute_reminder_offsets(100, 5)
|
||||
assert len(offsets) == 5
|
||||
assert all(o >= 1 for o in offsets)
|
||||
# Distinct offsets only (no duplicate alarms on the same day).
|
||||
assert len(set(offsets)) == len(offsets)
|
||||
|
||||
|
||||
def test_offsets_single_reminder_is_day_before_deadline():
|
||||
"""A single requested reminder fires one day before the deadline."""
|
||||
assert compute_reminder_offsets(30, 1) == [1]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# basic_reminder_offsets - fixed BASIC-mode offsets
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_basic_offsets_full_period():
|
||||
"""A far-away delivery keeps all three fixed offsets (30, 10, 1)."""
|
||||
assert basic_reminder_offsets(365) == [30, 10, 1]
|
||||
|
||||
|
||||
def test_basic_offsets_truncated_by_horizon():
|
||||
"""Offsets beyond the delivery horizon are dropped."""
|
||||
assert basic_reminder_offsets(20) == [10, 1]
|
||||
assert basic_reminder_offsets(5) == [1]
|
||||
|
||||
|
||||
def test_basic_offsets_empty():
|
||||
"""No future offsets when the delivery is less than a day away."""
|
||||
assert basic_reminder_offsets(0) == []
|
||||
assert basic_reminder_offsets(-10) == []
|
||||
|
||||
|
||||
def test_basic_offsets_always_from_fixed_set():
|
||||
"""Every returned offset is one of the fixed BASIC offsets."""
|
||||
for horizon in range(0, 40):
|
||||
result = basic_reminder_offsets(horizon)
|
||||
assert set(result).issubset(set(BASIC_REMINDER_OFFSETS))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# format_time
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_format_time_utc():
|
||||
dt = datetime(2025, 6, 1, 12, 30, 45, tzinfo=timezone.utc)
|
||||
assert format_time(dt) == "20250601T123045Z"
|
||||
|
||||
|
||||
def test_format_time_non_utc():
|
||||
tz = timezone(timedelta(hours=2))
|
||||
dt = datetime(2025, 6, 1, 12, 30, 45, tzinfo=tz)
|
||||
assert format_time(dt) == "20250601T103045Z"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# ical_escape
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_ical_escape_no_change():
|
||||
assert ical_escape("hello world") == "hello world"
|
||||
|
||||
|
||||
def test_ical_escape_backslash():
|
||||
assert ical_escape("a\\b") == "a\\\\b"
|
||||
|
||||
|
||||
def test_ical_escape_semicolon():
|
||||
assert ical_escape("a;b") == "a\\;b"
|
||||
|
||||
|
||||
def test_ical_escape_comma():
|
||||
assert ical_escape("a,b") == "a\\,b"
|
||||
|
||||
|
||||
def test_ical_escape_multiline():
|
||||
text = "line1\nline2"
|
||||
result = ical_escape(text)
|
||||
assert "line1" in result
|
||||
assert "line2" in result
|
||||
|
||||
|
||||
def test_ical_escape_all():
|
||||
assert ical_escape("\\;,") == "\\\\\\;\\,"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# fold_ical_line
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_fold_ical_line_short():
|
||||
assert fold_ical_line("SUMMARY:Test") == "SUMMARY:Test"
|
||||
|
||||
|
||||
def test_fold_ical_line_long():
|
||||
line = "SUMMARY:" + "a" * 100
|
||||
result = fold_ical_line(line, limit=75)
|
||||
parts = result.split("\r\n ")
|
||||
assert all(len(p.encode("utf-8")) <= 75 for p in parts)
|
||||
assert "".join(parts) == line
|
||||
|
||||
|
||||
def test_fold_ical_line_unicode():
|
||||
line = "SUMMARY:" + "é" * 50
|
||||
result = fold_ical_line(line, limit=75)
|
||||
parts = result.split("\r\n ")
|
||||
assert all(len(p.encode("utf-8")) <= 75 for p in parts)
|
||||
assert "".join(parts) == line
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# write_temp_ics
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_write_temp_ics():
|
||||
content = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"
|
||||
path = write_temp_ics(content)
|
||||
try:
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as f:
|
||||
assert f.read() == content.encode("utf-8")
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_write_temp_ics_empty():
|
||||
path = write_temp_ics("")
|
||||
try:
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as f:
|
||||
assert f.read() == b""
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# build_ics_reminders - unified builder
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
_LOCKTIME = datetime(2026, 7, 23, 9, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _common_kwargs():
|
||||
return dict(
|
||||
locktime=_LOCKTIME,
|
||||
description="BAL will of $wallet_name\r\n$heirs_complete",
|
||||
summary="BAL - Will execution of $wallet_name",
|
||||
wallet_name="karen7",
|
||||
heirs_details=" alice - bc1qalice, bob - bc1qbob",
|
||||
version="0.6.1",
|
||||
)
|
||||
|
||||
|
||||
def test_build_ics_advanced_separate_events():
|
||||
"""ADVANCED: 30-day period -> offsets [30, 16, 1], one VEVENT each."""
|
||||
content = build_ics_reminders(
|
||||
basic_mode=False,
|
||||
num_reminders=3,
|
||||
now=datetime(2026, 7, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
threshold=datetime(2026, 6, 23, 0, 0, 0, tzinfo=timezone.utc),
|
||||
**_common_kwargs(),
|
||||
)
|
||||
assert content is not None
|
||||
assert content.startswith("BEGIN:VCALENDAR")
|
||||
assert content.endswith("\r\n")
|
||||
lines = content.split("\r\n")
|
||||
assert lines.count("BEGIN:VEVENT") == 3
|
||||
assert lines.count("END:VEVENT") == 3
|
||||
assert "BEGIN:VALARM" not in lines
|
||||
# Unique UIDs, numbered summaries, last event one day before the deadline.
|
||||
uids = [ln for ln in lines if ln.startswith("UID:")]
|
||||
assert len(uids) == len(set(uids)) == 3
|
||||
assert any("(reminder 1/3)" in ln for ln in lines)
|
||||
assert any("(reminder 3/3)" in ln for ln in lines)
|
||||
last_dt = format_time(_LOCKTIME - timedelta(days=1))
|
||||
assert f"DTSTART:{last_dt}" in lines
|
||||
# Template substitution; the CRLF inside the description stays as a line
|
||||
# break in the folded DESCRIPTION (no literal "\n" escaping in ical_escape).
|
||||
desc = [ln for ln in lines if ln.startswith("DESCRIPTION:")]
|
||||
assert desc and "karen7" in desc[0]
|
||||
assert any(ln.lstrip().startswith("alice") for ln in lines)
|
||||
|
||||
def test_build_ics_basic_uses_fixed_offsets():
|
||||
"""BASIC: fixed offsets (30, 10, 1) filtered to the future, threshold not
|
||||
required."""
|
||||
content = build_ics_reminders(
|
||||
basic_mode=True,
|
||||
now=datetime(2026, 6, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
**_common_kwargs(),
|
||||
)
|
||||
assert content is not None
|
||||
lines = content.split("\r\n")
|
||||
uids = [ln for ln in lines if ln.startswith("UID:")]
|
||||
assert len(uids) == 3
|
||||
# The first event is the 30-day offset.
|
||||
assert "bal-karen7-30d" in uids[0]
|
||||
|
||||
|
||||
def test_build_ics_returns_none_when_no_reminders():
|
||||
"""No future reminder -> None (the aligned behavior), not an empty file."""
|
||||
content = build_ics_reminders(
|
||||
basic_mode=True,
|
||||
now=datetime(2026, 7, 23, 0, 0, 0, tzinfo=timezone.utc),
|
||||
**_common_kwargs(),
|
||||
)
|
||||
assert content is None
|
||||
|
||||
|
||||
def test_build_ics_advanced_requires_threshold():
|
||||
"""ADVANCED mode without a threshold is a programming error."""
|
||||
try:
|
||||
build_ics_reminders(basic_mode=False, **_common_kwargs())
|
||||
except ValueError:
|
||||
return
|
||||
raise AssertionError("expected ValueError for missing threshold in ADVANCED")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Main
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All core reminders tests passed")
|
||||
@@ -10,13 +10,12 @@ Covered behaviour:
|
||||
most one reminder per available day, and never returns more reminders than
|
||||
requested.
|
||||
|
||||
``compute_reminder_offsets`` lives in ``bal.gui.qt.widgets`` (which imports
|
||||
PyQt6), so these tests are run headless with ``QT_QPA_PLATFORM=offscreen`` like
|
||||
the other GUI tests.
|
||||
``compute_reminder_offsets`` now lives in ``bal.core.reminders`` (pure, GUI-free),
|
||||
so these tests run without Qt (or Electrum) at all.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
||||
python3 -m pytest tests/test_group_d_alarms.py -q
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
python3 tests/test_group_d_alarms.py
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -25,7 +24,7 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.core.plugin_base import BalConfig
|
||||
from bal.gui.qt.widgets import compute_reminder_offsets
|
||||
from bal.core.reminders import compute_reminder_offsets
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Mocks
|
||||
|
||||
@@ -37,10 +37,14 @@ import pytest # pyright: ignore[reportMissingImports]
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.core.heirs import Heirs
|
||||
from bal.core.reminders import (
|
||||
compute_reminder_offsets,
|
||||
format_time,
|
||||
ical_escape,
|
||||
write_temp_ics,
|
||||
)
|
||||
from bal.core.will import HeirNotFoundException, Will, WillItem
|
||||
from bal.core.willexecutors import Willexecutors
|
||||
from bal.gui.qt.calendar import BalCalendar
|
||||
from bal.gui.qt.widgets import compute_reminder_offsets
|
||||
|
||||
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output,
|
||||
# version 2). Reused from test_core_will.py so WillItem can parse a real tx.
|
||||
@@ -196,8 +200,8 @@ def test_e1_build_separate_events_for_karen7():
|
||||
|
||||
lines = ["BEGIN:VCALENDAR", "VERSION:2.0"]
|
||||
for idx, offset in enumerate(offsets, start=1):
|
||||
event_dt = BalCalendar.format_time(locktime - timedelta(days=offset))
|
||||
summary = BalCalendar.ical_escape(f"{summary_base} (reminder {idx}/{total})")
|
||||
event_dt = format_time(locktime - timedelta(days=offset))
|
||||
summary = ical_escape(f"{summary_base} (reminder {idx}/{total})")
|
||||
lines.extend([
|
||||
"BEGIN:VEVENT",
|
||||
f"UID:bal-{wallet}-{offset}d",
|
||||
@@ -219,7 +223,7 @@ def test_e1_build_separate_events_for_karen7():
|
||||
assert any("(reminder 1/3)" in ln for ln in lines)
|
||||
assert any("(reminder 3/3)" in ln for ln in lines)
|
||||
# The LAST event (offset 1) sits one day before the locktime.
|
||||
last_dt = BalCalendar.format_time(locktime - timedelta(days=1))
|
||||
last_dt = format_time(locktime - timedelta(days=1))
|
||||
assert f"DTSTART:{last_dt}" in lines
|
||||
|
||||
|
||||
@@ -227,7 +231,7 @@ def test_e1_event_description_escaping():
|
||||
"""Special iCalendar characters in karen7's event text are escaped so
|
||||
the .ics file stays valid."""
|
||||
raw = "Wallet karen7; heirs: alice, bob"
|
||||
escaped = BalCalendar.ical_escape(raw)
|
||||
escaped = ical_escape(raw)
|
||||
assert "\\;" in escaped # semicolon escaped
|
||||
assert "\\," in escaped # comma escaped
|
||||
assert "karen7" in escaped
|
||||
@@ -245,7 +249,7 @@ def test_e1_write_temp_ics_for_karen7():
|
||||
"END:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n"
|
||||
)
|
||||
path = BalCalendar.write_temp_ics(content)
|
||||
path = write_temp_ics(content)
|
||||
try:
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as f:
|
||||
|
||||
@@ -10,13 +10,12 @@ that would fall in the past.
|
||||
These tests pin the behaviour of the pure helper ``basic_reminder_offsets`` that
|
||||
drives that decision.
|
||||
|
||||
``basic_reminder_offsets`` lives in ``bal.gui.qt.widgets`` (which imports
|
||||
PyQt6), so these tests are run headless with ``QT_QPA_PLATFORM=offscreen`` like
|
||||
the other GUI tests.
|
||||
``basic_reminder_offsets`` now lives in ``bal.core.reminders`` (pure, GUI-free),
|
||||
so these tests run without Qt (or Electrum) at all.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
||||
python3 -m pytest tests/test_group_g_basic_calendar.py -q
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
python3 tests/test_group_g_basic_calendar.py
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -24,7 +23,7 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.gui.qt.widgets import BASIC_REMINDER_OFFSETS, basic_reminder_offsets
|
||||
from bal.core.reminders import BASIC_REMINDER_OFFSETS, basic_reminder_offsets
|
||||
|
||||
|
||||
def test_basic_offsets_all_future():
|
||||
|
||||
@@ -3,116 +3,99 @@ Tests for the "BASIC mode dynamic Check Alive" fix (Group I).
|
||||
|
||||
Background (reported by the owner): with the plugin left in BASIC mode (the
|
||||
default), the "Check Alive" (threshold) field is hidden from the user and
|
||||
stays stuck at its old/default value (roughly "today + 11 months", i.e. one
|
||||
year minus the default 30-day margin). If the user then anticipates the
|
||||
delivery time (locktime) to something earlier than that stale threshold - e.g.
|
||||
"in 1 month" - two different checks that compare locktime against
|
||||
``date_to_check`` (the resolved threshold) would incorrectly treat the will as
|
||||
"expired"/"invalid", even though the whole Check Alive concept is supposed to
|
||||
be inert in BASIC mode.
|
||||
stays stuck at its old/default value. If the user then anticipates the
|
||||
delivery time (locktime) to something earlier than that stale threshold, the
|
||||
checks that compare locktime against ``date_to_check`` would incorrectly treat
|
||||
the will as "expired"/"invalid", even though the whole Check Alive concept is
|
||||
supposed to be inert in BASIC mode.
|
||||
|
||||
The fix (``BalWindow.compute_date_to_check``) makes ``date_to_check`` track
|
||||
the delivery time LIVE in BASIC mode, placed a fixed 2-hour margin before it,
|
||||
so it is always < locktime by construction. In ADVANCED mode nothing changes:
|
||||
the stored threshold is used as-is.
|
||||
The real fix lives in ``BalWindow.init_class_variables`` (window.py), which
|
||||
sets ``date_to_check`` to *now* in BASIC mode, and is now implemented by the
|
||||
pure policy in ``bal.core.checkalive``:
|
||||
|
||||
These tests call the real production method directly (no GUI/Electrum wallet
|
||||
needed - it is a plain ``@staticmethod``), so they exercise the exact code
|
||||
used at runtime rather than a re-implementation.
|
||||
* ``resolve_date_to_check(is_basic_mode, will_settings)`` - the single
|
||||
reference timestamp: "now" in BASIC, the stored threshold in ADVANCED;
|
||||
* ``check_alive_expired(is_basic_mode, date_to_check)`` - whether the
|
||||
Check-Alive guard should fire (never in BASIC).
|
||||
|
||||
These tests exercise the exact code used at runtime (no GUI/Electrum wallet
|
||||
needed, and no Qt import).
|
||||
|
||||
Run:
|
||||
PYTHONPATH=electrum-src python3 -m pytest tests/test_group_i_basic_checkalive.py -q
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
python3 tests/test_group_i_basic_checkalive.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
|
||||
|
||||
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
|
||||
from bal.core.checkalive import ( # noqa: E402 (path insert above)
|
||||
CheckAliveError,
|
||||
check_alive_expired,
|
||||
resolve_date_to_check,
|
||||
)
|
||||
|
||||
|
||||
def test_offset_is_two_hours():
|
||||
"""Owner-approved margin: exactly 2 hours."""
|
||||
assert OFFSET == 2 * 60 * 60
|
||||
def test_basic_mode_resolves_to_now():
|
||||
"""BASIC mode: date_to_check is "now", never the stale stored threshold."""
|
||||
fake_now = 1_800_000_000.0
|
||||
result = resolve_date_to_check(True, {"threshold": time.time() + 86400}, now=fake_now)
|
||||
assert result == fake_now
|
||||
|
||||
|
||||
def _relative_days_to_midnight_timestamp(days):
|
||||
"""Reproduce Util.parse_locktime_string's own normalisation: "now + N
|
||||
days", truncated to midnight. Used to compute the expected value in
|
||||
tests without duplicating the parsing logic itself."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime.now()
|
||||
return (
|
||||
(now + timedelta(days=days))
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
.timestamp()
|
||||
)
|
||||
def test_basic_mode_ignores_stored_threshold():
|
||||
"""BASIC + delivery anticipated to 1 month: date_to_check stays "now" and
|
||||
does NOT jump to the old ~11-month threshold - this is the exact bug
|
||||
scenario reported by the owner."""
|
||||
stale_threshold = time.time() + 11 * 30 * 86400 # the old stuck value
|
||||
fake_now = 1_800_000_000.0
|
||||
result = resolve_date_to_check(True, {"threshold": stale_threshold}, now=fake_now)
|
||||
assert result == fake_now
|
||||
|
||||
|
||||
def test_basic_mode_relative_locktime_one_year():
|
||||
"""BASIC + default "1y" delivery: date_to_check tracks it, 2h earlier."""
|
||||
result = BalWindow.compute_date_to_check(True, "1y", "30d")
|
||||
expected_locktime = _relative_days_to_midnight_timestamp(365)
|
||||
assert abs(result - (expected_locktime - OFFSET)) < 5
|
||||
|
||||
|
||||
def test_basic_mode_anticipated_delivery_stays_consistent():
|
||||
"""BASIC + delivery anticipated to 1 month: date_to_check follows it,
|
||||
NOT the old ~11-month threshold - this is the exact bug scenario
|
||||
reported by the owner."""
|
||||
stale_threshold = "30d" # would resolve close to the OLD 1-year locktime
|
||||
anticipated_locktime = "30d" # user moved delivery to ~1 month from now
|
||||
result = BalWindow.compute_date_to_check(
|
||||
True, anticipated_locktime, stale_threshold
|
||||
)
|
||||
locktime_ts = _relative_days_to_midnight_timestamp(30)
|
||||
# date_to_check must be (delivery - 2h), always strictly before delivery.
|
||||
assert result < locktime_ts
|
||||
assert abs((locktime_ts - OFFSET) - result) < 5
|
||||
|
||||
|
||||
def test_basic_mode_date_to_check_always_before_locktime():
|
||||
"""Regression guard for the reported bug: whatever the delivery date is
|
||||
(even very close to "now"), date_to_check must stay before it."""
|
||||
for relative_locktime in ("1d", "7d", "30d", "90d", "365d"):
|
||||
result = BalWindow.compute_date_to_check(True, relative_locktime, "30d")
|
||||
parsed_locktime = _relative_days_to_midnight_timestamp(
|
||||
int(relative_locktime[:-1])
|
||||
)
|
||||
assert result < parsed_locktime, (
|
||||
f"date_to_check ({result}) should be before locktime "
|
||||
f"({parsed_locktime}) for locktime={relative_locktime}"
|
||||
)
|
||||
|
||||
|
||||
def test_advanced_mode_uses_stored_threshold_unchanged():
|
||||
"""ADVANCED mode: behaviour must stay exactly as before this fix - the
|
||||
stored threshold is used as-is, regardless of the locktime value."""
|
||||
def test_advanced_mode_uses_stored_threshold():
|
||||
"""ADVANCED mode: behaviour stays exactly as before - the stored threshold
|
||||
is used as-is, regardless of the locktime value."""
|
||||
absolute_threshold = time.time() + 5 * 86400 # arbitrary user-chosen value
|
||||
result = BalWindow.compute_date_to_check(False, "30d", absolute_threshold)
|
||||
result = resolve_date_to_check(False, {"threshold": absolute_threshold})
|
||||
assert abs(result - absolute_threshold) < 1
|
||||
|
||||
|
||||
def test_basic_mode_falls_back_on_unparsable_locktime():
|
||||
"""If the locktime can't be parsed for any reason, BASIC mode must not
|
||||
crash: it falls back to the stored threshold, same as ADVANCED."""
|
||||
absolute_threshold = time.time() + 5 * 86400
|
||||
result = BalWindow.compute_date_to_check(
|
||||
True, {"not": "a valid locktime"}, absolute_threshold
|
||||
)
|
||||
assert abs(result - absolute_threshold) < 1
|
||||
def test_advanced_mode_parses_relative_threshold():
|
||||
"""ADVANCED + relative threshold ("30d") resolves to a future timestamp."""
|
||||
result = resolve_date_to_check(False, {"threshold": "30d"})
|
||||
assert result > time.time()
|
||||
|
||||
|
||||
def test_basic_mode_never_expired():
|
||||
"""BASIC mode can never be "expired": a passed check-alive date must never
|
||||
force a postpone/rewrite of the will."""
|
||||
assert check_alive_expired(True, time.time() - 10_000) is False
|
||||
assert check_alive_expired(True, 1_000_000_000.0) is False
|
||||
|
||||
|
||||
def test_advanced_mode_expired_when_past():
|
||||
assert check_alive_expired(False, time.time() - 10_000) is True
|
||||
|
||||
|
||||
def test_advanced_mode_not_expired_when_future():
|
||||
assert check_alive_expired(False, time.time() + 10_000) is False
|
||||
|
||||
|
||||
def test_basic_mode_never_raises_check_alive_error():
|
||||
"""Regression guard for the reported bug: whatever the delivery date,
|
||||
BASIC mode never raises CheckAliveError (the postpone/invalidate trigger)."""
|
||||
date_to_check = resolve_date_to_check(True, {"threshold": "30d"})
|
||||
assert not check_alive_expired(True, date_to_check)
|
||||
# If it did fire, this is the exact exception that would be raised.
|
||||
assert issubclass(CheckAliveError, Exception)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_offset_is_two_hours()
|
||||
test_basic_mode_relative_locktime_one_year()
|
||||
test_basic_mode_anticipated_delivery_stays_consistent()
|
||||
test_basic_mode_date_to_check_always_before_locktime()
|
||||
test_advanced_mode_uses_stored_threshold_unchanged()
|
||||
test_basic_mode_falls_back_on_unparsable_locktime()
|
||||
print("All test_group_i_basic_checkalive tests passed.")
|
||||
for name in sorted(dir()):
|
||||
if name.startswith("test_"):
|
||||
globals()[name]()
|
||||
print(f" [OK] {name}")
|
||||
print("[OK] All test_group_i_basic_checkalive tests passed.")
|
||||
|
||||
@@ -1,126 +1,25 @@
|
||||
"""
|
||||
Tests for ``bal.gui.qt.calendar``.
|
||||
|
||||
Covers BalCalendar static methods: format_time, ical_escape, fold_ical_line,
|
||||
write_temp_ics, open_with_default_app.
|
||||
Covers the GUI/OS glue that stayed in this module: BalCalendar.open_with_default_app.
|
||||
The RFC-5545 helpers (format_time, ical_escape, fold_ical_line, write_temp_ics)
|
||||
moved to ``bal.core.reminders`` and are tested in ``tests/test_core_reminders.py``.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_calendar.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.gui.qt.calendar import BalCalendar
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# format_time
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_format_time_utc():
|
||||
dt = datetime(2025, 6, 1, 12, 30, 45, tzinfo=timezone.utc)
|
||||
assert BalCalendar.format_time(dt) == "20250601T123045Z"
|
||||
|
||||
|
||||
def test_format_time_non_utc():
|
||||
from datetime import timedelta
|
||||
tz = timezone(timedelta(hours=2))
|
||||
dt = datetime(2025, 1, 15, 8, 0, 0, tzinfo=tz)
|
||||
result = BalCalendar.format_time(dt)
|
||||
assert result.endswith("Z")
|
||||
assert result == "20250115T060000Z"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# ical_escape
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_ical_escape_no_change():
|
||||
text = "hello world"
|
||||
assert BalCalendar.ical_escape(text) == "hello world"
|
||||
|
||||
|
||||
def test_ical_escape_backslash():
|
||||
assert BalCalendar.ical_escape("a\\b") == "a\\\\b"
|
||||
|
||||
|
||||
def test_ical_escape_semicolon():
|
||||
assert BalCalendar.ical_escape("a;b") == "a\\;b"
|
||||
|
||||
|
||||
def test_ical_escape_comma():
|
||||
assert BalCalendar.ical_escape("a,b") == "a\\,b"
|
||||
|
||||
|
||||
def test_ical_escape_multiline():
|
||||
text = "line1\r\nline2"
|
||||
result = BalCalendar.ical_escape(text)
|
||||
assert "\r\n" in result
|
||||
assert "line1" in result
|
||||
assert "line2" in result
|
||||
|
||||
|
||||
def test_ical_escape_all():
|
||||
text = "\\;,"
|
||||
assert BalCalendar.ical_escape(text) == "\\\\\\;\\,"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# fold_ical_line
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_fold_ical_line_short():
|
||||
line = "SUMMARY:Test"
|
||||
assert BalCalendar.fold_ical_line(line) == "SUMMARY:Test"
|
||||
|
||||
|
||||
def test_fold_ical_line_long():
|
||||
line = "X-LONG:" + "a" * 100
|
||||
result = BalCalendar.fold_ical_line(line, limit=75)
|
||||
parts = result.split("\r\n ")
|
||||
assert len(parts) > 1
|
||||
assert result.startswith("X-LONG:")
|
||||
|
||||
|
||||
def test_fold_ical_line_unicode():
|
||||
line = "DESCRIPTION:" + "\u20ac" * 40
|
||||
result = BalCalendar.fold_ical_line(line, limit=75)
|
||||
assert "\r\n " in result
|
||||
assert "\u20ac" in result
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# write_temp_ics
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_write_temp_ics():
|
||||
content = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"
|
||||
path = BalCalendar.write_temp_ics(content)
|
||||
try:
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as f:
|
||||
assert f.read() == content.encode("utf-8")
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_write_temp_ics_empty():
|
||||
path = BalCalendar.write_temp_ics("")
|
||||
try:
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as f:
|
||||
assert f.read() == b""
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# open_with_default_app
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
||||
def test_open_with_default_app_not_found():
|
||||
result = BalCalendar.open_with_default_app(
|
||||
"/nonexistent/calendar_app", "/tmp/fake.ics"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
Tests for ``bal.gui.qt.common``.
|
||||
|
||||
Covers shown_cv, CheckAliveError, add_widget, log_error, export_meta_gui.
|
||||
Covers shown_cv, add_widget, log_error, export_meta_gui (and the
|
||||
CheckAliveError exception, which now lives in ``bal.core.checkalive``).
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py
|
||||
@@ -44,23 +45,30 @@ def test_shown_cv_roundtrip():
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# CheckAliveError
|
||||
# CheckAliveError (moved to bal.core.checkalive; kept tested via the
|
||||
# common->core import chain)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_check_alive_error_default():
|
||||
err = common.CheckAliveError(1000000)
|
||||
from bal.core.checkalive import CheckAliveError
|
||||
|
||||
err = CheckAliveError(1000000)
|
||||
assert err.timestamp_to_check == 1000000
|
||||
|
||||
|
||||
def test_check_alive_error_str():
|
||||
err = common.CheckAliveError(1000000)
|
||||
from bal.core.checkalive import CheckAliveError
|
||||
|
||||
err = CheckAliveError(1000000)
|
||||
s = str(err)
|
||||
assert "Check alive expired" in s
|
||||
assert "1970" in s
|
||||
|
||||
|
||||
def test_check_alive_error_subclass():
|
||||
assert issubclass(common.CheckAliveError, Exception)
|
||||
from bal.core.checkalive import CheckAliveError
|
||||
|
||||
assert issubclass(CheckAliveError, Exception)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -118,23 +118,25 @@ def test_locktime_editor_min_max():
|
||||
def test_locktime_raw_edit_replace_str():
|
||||
# replace_str only strips the day ("d") and year ("y") suffixes. The
|
||||
# block-height suffix ("b") was removed (A1), so "b" is NOT stripped
|
||||
# anymore (locktimes are always UNIX timestamps now).
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
assert LockTimeRawEdit.replace_str("123d") == "123"
|
||||
assert LockTimeRawEdit.replace_str("456y") == "456"
|
||||
# anymore (locktimes are always UNIX timestamps now). The helper moved to
|
||||
# bal.core.input_rules (replace_dy_suffixes); the widget delegates to it.
|
||||
from bal.core.input_rules import replace_dy_suffixes
|
||||
assert replace_dy_suffixes("123d") == "123"
|
||||
assert replace_dy_suffixes("456y") == "456"
|
||||
# "b" is left untouched (no longer a recognised suffix)
|
||||
assert LockTimeRawEdit.replace_str("789b") == "789b"
|
||||
assert replace_dy_suffixes("789b") == "789b"
|
||||
# only d/y are stripped; a stray "b" remains
|
||||
assert LockTimeRawEdit.replace_str("12d34y56b") == "123456b"
|
||||
assert replace_dy_suffixes("12d34y56b") == "123456b"
|
||||
|
||||
|
||||
def test_locktime_raw_edit_checkbdy():
|
||||
from bal.gui.qt.widgets import LockTimeRawEdit
|
||||
# checkbdy moved to bal.core.input_rules (_checkbdy).
|
||||
from bal.core.input_rules import _checkbdy
|
||||
# character at expected position matches appendix
|
||||
pos, s = LockTimeRawEdit.checkbdy(None, "123d", 4, "d")
|
||||
pos, s = _checkbdy("123d", 4, "d")
|
||||
assert s == "123d"
|
||||
# character at expected position does not match
|
||||
pos, s = LockTimeRawEdit.checkbdy(None, "123x", 4, "d")
|
||||
pos, s = _checkbdy("123x", 4, "d")
|
||||
assert s == "123x"
|
||||
|
||||
|
||||
|
||||
@@ -305,10 +305,14 @@ def _make_merge_fake(willitems):
|
||||
wallet=FakeWallet(),
|
||||
bal_window=None,
|
||||
date_to_check=1700000000,
|
||||
will_settings={"threshold": 1700000000},
|
||||
bal_plugin=SimpleNamespace(
|
||||
HISTORY_LABEL=SimpleNamespace(
|
||||
get=lambda: "BAL will history ({willexecutor})"
|
||||
)
|
||||
),
|
||||
# BASIC is the default user type; merge_will resolves date_to_check
|
||||
# through bal.core.checkalive when it is missing.
|
||||
is_basic_mode=lambda: True,
|
||||
),
|
||||
update_all=lambda: calls.append("update_all"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user