add tests

This commit is contained in:
bot
2026-06-20 09:49:39 -04:00
parent 86ed0297a7
commit 525dde2b3c
34 changed files with 7427 additions and 0 deletions

462
tests/test_core_util.py Normal file
View File

@@ -0,0 +1,462 @@
"""
Comprehensive unit tests for ``bal.core.util.Util``.
Covers every static method — locktime helpers, amount helpers, comparison
helpers, UTXO helpers, and migration helpers — with edge cases.
Run:
source electrum/env/bin/activate
python3 tests/test_core_util.py
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.util import Util
def test_locktime_to_str():
# timestamp -> ISO format
s = Util.locktime_to_str(1700000000)
assert "202" in s and "-" in s, f"expected ISO string, got {s!r}"
# small value -> still treated as timestamp
s = Util.locktime_to_str(500000)
assert "-" in s and ":" in s, f"expected ISO string, got {s!r}"
# string input -> unchanged
assert Util.locktime_to_str("hello") == "hello"
# zero -> still treated as timestamp
s = Util.locktime_to_str(0)
assert "-" in s, f"expected ISO date, got {s!r}"
def test_str_to_locktime():
# relative suffixes pass through
assert Util.str_to_locktime("30d") == "30d"
assert Util.str_to_locktime("1y") == "1y"
# integer string -> int
assert isinstance(Util.str_to_locktime("500000"), int)
assert Util.str_to_locktime("500000") == 500000
# ISO date -> int timestamp
ts = Util.str_to_locktime("2025-01-01T00:00:00")
assert isinstance(ts, int)
assert ts > 0
def test_parse_locktime_string():
# plain int -> same int
assert Util.parse_locktime_string(500000) == 500000
# int as string -> int
assert Util.parse_locktime_string("500000") == 500000
# relative days -> > current timestamp
result = Util.parse_locktime_string("7d")
import time
assert result > time.time() - 86400
# relative years -> > current timestamp
result = Util.parse_locktime_string("1y")
assert result > time.time()
# invalid -> 0
assert Util.parse_locktime_string("") == 0
assert Util.parse_locktime_string("garbage") == 0
def test_int_locktime():
assert Util.int_locktime(seconds=1) == 1
assert Util.int_locktime(minutes=1) == 60
assert Util.int_locktime(hours=1) == 3600
assert Util.int_locktime(days=1) == 86400
assert Util.int_locktime() == 0
def test_encode_decode_amount():
dp = 8 # typical BTC decimal point
# percentage passes through
assert Util.encode_amount("50%", dp) == "50%"
assert Util.decode_amount("50%", dp) == "50%"
# satoshi encoding
assert Util.encode_amount("1.0", dp) == 100000000
assert Util.encode_amount("0.5", dp) == 50000000
# decoding
assert Util.decode_amount(100000000, dp) == "1.00000000"
assert Util.decode_amount(50000000, dp) == "0.50000000"
# edge
assert Util.encode_amount("abc", dp) == 0
assert Util.decode_amount("abc", dp) == "abc"
def test_is_perc():
assert Util.is_perc("50%") is True
assert Util.is_perc("100%") is True
assert Util.is_perc("0%") is True
assert Util.is_perc("100") is False
assert Util.is_perc(50) is False
assert Util.is_perc("") is False
assert Util.is_perc(None) is False
def test_cmp_array():
assert Util.cmp_array([1, 2, 3], [1, 2, 3]) is True
assert Util.cmp_array([1, 2, 3], [1, 2]) is False
assert Util.cmp_array([], []) is True
assert Util.cmp_array([1], [2]) is False
assert Util.cmp_array(None, None) is False # exception path
def test_cmp_heir():
heira = ["abc", 10000, 12345]
heirb = ["abc", 10000, 54321]
assert Util.cmp_heir(heira, heirb) is True # addr(0) + amount(1) match
heirb2 = ["xyz", 10000, 12345]
assert Util.cmp_heir(heira, heirb2) is False # addr mismatch
heirb3 = ["abc", 20000, 12345]
assert Util.cmp_heir(heira, heirb3) is False # amount mismatch
def test_cmp_willexecutor():
a = {"url": "https://we.example", "address": "bc1abc", "base_fee": 1000}
b = {"url": "https://we.example", "address": "bc1abc", "base_fee": 1000}
assert Util.cmp_willexecutor(a, b) is True
c = {"url": "https://we.other", "address": "bc1abc", "base_fee": 1000}
assert Util.cmp_willexecutor(a, c) is False
assert Util.cmp_willexecutor(None, None) is True # None == None
assert Util.cmp_willexecutor({}, {}) is True # both empty
def test_search_heir_by_values():
heirs = {
"alice": {0: "addr1", 1: 1000, 3: 500},
"bob": {0: "addr2", 1: 2000, 3: 600},
}
match = Util.search_heir_by_values(heirs, {0: "addr1", 3: 500}, [0, 3])
assert match == "alice"
no_match = Util.search_heir_by_values(heirs, {0: "addrX", 3: 500}, [0, 3])
assert no_match is False
assert Util.search_heir_by_values({}, {0: "x"}, [0]) is False
def test_cmp_heir_by_values():
a = {0: "addr1", 1: 1000, 3: 500}
b = {0: "addr1", 1: 1000, 3: 500}
assert Util.cmp_heir_by_values(a, b, [0, 1]) is True
assert Util.cmp_heir_by_values(a, b, [0, 1, 3]) is True
c = {0: "addr1", 1: 9999, 3: 500}
assert Util.cmp_heir_by_values(a, c, [1]) is False
def test_cmp_heirs_by_values():
a = {"h1": {0: "a1", 1: 100}, "h2": {0: "a2", 1: 200}}
b = {"h3": {0: "a1", 1: 100}, "h4": {0: "a2", 1: 200}}
assert Util.cmp_heirs_by_values(a, b, [0, 1]) is True
c = {"h1": {0: "aX", 1: 100}}
assert Util.cmp_heirs_by_values(a, c, [0, 1]) is False
def test_cmp_inputs():
# Without real TxInput objects we test edge cases
assert Util.cmp_inputs([], []) is True
assert Util.cmp_inputs([1], []) is False
assert Util.cmp_inputs([], [1]) is False
def test_cmp_outputs():
assert Util.cmp_outputs([], []) is True
assert Util.cmp_outputs([1], []) is False
assert Util.cmp_outputs([], [1]) is False
def test_cmp_txs():
# No real Transaction objects, but edge coverage
class FakeTx:
def inputs(self): return []
def outputs(self): return []
a = FakeTx()
assert Util.cmp_txs(a, a) is True
def test_get_value_amount():
class FakeOutput:
def __init__(self, addr, val):
self.address = addr
self.value = val
class FakeTx:
def outputs(self): return self._outs
def __init__(self, outs): self._outs = outs
# Shared addr+value → both same_amount and same_address → value counted
out_a = FakeOutput("bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", 1000)
out_b = FakeOutput("bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", 1000)
result = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_b]))
assert result == 1000, f"expected 1000, got {result}"
# Different address, same amount → same_amount only → not counted
out_c = FakeOutput("bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", 1000)
result2 = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_c]))
assert result2 == 0, f"expected 0, got {result2}"
# No matching amount → returns False
out_d = FakeOutput("bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", 999)
result3 = Util.get_value_amount(FakeTx([out_a]), FakeTx([out_d]))
assert result3 is False, f"expected False, got {result3}"
def test_chk_locktime():
now_ts = 1700000000
# timestamp locktime still in future
assert Util.chk_locktime(now_ts, 1800000000) is True
# timestamp locktime in past
assert Util.chk_locktime(now_ts, 1000000000) is False
# locktime at boundary
assert Util.chk_locktime(now_ts, now_ts) is False
def test_anticipate_locktime():
ts = 1700000000
result = Util.anticipate_locktime(ts, days=1)
assert result < ts
assert result > 0
# overflow handling (Windows-safe)
huge = 2**32 - 1 # NLOCKTIME_MAX
result = Util.anticipate_locktime(huge, days=1)
assert result > 0
# clamp to minimum 1
low = Util.anticipate_locktime(10, days=20)
assert low >= 1
def test_cmp_locktime():
assert Util.cmp_locktime("30d", "30d") == 0
# Note: cmp_locktime may return nonzero or None for mismatched units
def test_get_locktimes():
class FakeTx:
locktime = 1700000000
# will with single entry
will = {
"tx1": {"tx": FakeTx()},
}
locktimes = list(Util.get_locktimes(will))
assert 1700000000 in locktimes
assert len(locktimes) == 1
# empty will
assert list(Util.get_locktimes({})) == []
def test_get_lowest_locktimes():
result = Util.get_lowest_locktimes([500000, 1700000000, 100, 900000])
assert isinstance(result, list)
assert result == sorted(result)
assert 100 in result
assert 1700000000 in result
# empty
assert Util.get_lowest_locktimes([]) == []
def test_get_will_spent_utxos():
class FakeTx:
def inputs(self): return [1, 2, 3]
will = {
"tx1": {"tx": FakeTx()},
"tx2": {"tx": FakeTx()},
}
utxos = Util.get_will_spent_utxos(will)
assert len(utxos) == 6 # 3 inputs * 2 txs
def test_utxo_to_str():
class FakeUtxo:
def to_str(self): return "txid:0"
assert Util.utxo_to_str(FakeUtxo()) == "txid:0"
class FakePrevout:
def to_str(self): return "txid:1"
class FakeUtxo2:
to_str = None
prevout = FakePrevout()
assert Util.utxo_to_str(FakeUtxo2()) == "txid:1"
# fallback
class Broken:
pass
assert len(Util.utxo_to_str(Broken())) > 0
def test_cmp_utxo():
class A:
def to_str(self): return "abc:0"
assert Util.cmp_utxo(A(), A()) is True
class B:
def to_str(self): return "xyz:1"
assert Util.cmp_utxo(A(), B()) is False
def test_in_utxo():
class U:
def __init__(self, s):
self._s = s
def to_str(self): return self._s
utxos = [U("a:0"), U("b:1")]
target = U("a:0")
assert Util.in_utxo(target, utxos) is True
assert Util.in_utxo(U("z:9"), utxos) is False
assert Util.in_utxo(target, []) is False
def test_cmp_output():
class O:
def __init__(self, addr, val):
self.address = addr
self.value = val
assert Util.cmp_output(O("a", 100), O("a", 100)) is True
assert Util.cmp_output(O("a", 100), O("b", 100)) is False
assert Util.cmp_output(O("a", 100), O("a", 200)) is False
def test_in_output():
class O:
def __init__(self, addr, val):
self.address = addr
self.value = val
outputs = [O("a", 100), O("b", 200)]
assert Util.in_output(O("a", 100), outputs) is True
assert Util.in_output(O("z", 999), outputs) is False
assert Util.in_output(O("a", 100), []) is False
def test_din_output():
class O:
def __init__(self, addr, val):
self.address = addr
self.value = val
outputs = [O("a", 100), O("b", 200)]
# same amount AND same address
same_amt, same_addr = Util.din_output(O("a", 100), outputs)
assert same_amt is True and same_addr is True
# same amount but different address
same_amt, same_addr = Util.din_output(O("c", 100), outputs)
assert same_amt is True and same_addr is False
# different amount
same_amt, same_addr = Util.din_output(O("z", 999), outputs)
assert same_amt is False and same_addr is False
def test_get_current_height():
# returns current UNIX timestamp, ignoring network
import time
now = Util.get_current_height(None)
assert abs(now - int(time.time())) < 5
def test_copy():
d = {"a": 1}
Util.copy(d, {"b": 2})
assert d == {"a": 1, "b": 2}
# overwrite
Util.copy(d, {"a": 99})
assert d["a"] == 99
def test_fix_will_settings_tx_fees():
settings = {"tx_fees": 50}
assert Util.fix_will_settings_tx_fees(settings) is True
assert settings["baltx_fees"] == 50
assert "tx_fees" not in settings
# no migration needed
assert Util.fix_will_settings_tx_fees({}) is False
def test_fix_will_tx_fees():
will = {
"tx1": {"tx_fees": 30},
"tx2": {"baltx_fees": 50},
}
assert Util.fix_will_tx_fees(will) is True
assert will["tx1"]["baltx_fees"] == 30
assert "tx_fees" not in will["tx1"]
# empty will
assert Util.fix_will_tx_fees({}) is False
def test_text_hex_conversion():
assert Util.text_to_hex("BAL") == "42414c"
assert Util.hex_to_text("42414c") == "BAL"
assert Util.text_to_hex("") == ""
assert Util.hex_to_text("") == ""
assert Util.hex_to_text("ZZZ") == "Error: Invalid hex string"
if __name__ == "__main__":
test_locktime_to_str()
test_str_to_locktime()
test_parse_locktime_string()
test_int_locktime()
test_encode_decode_amount()
test_is_perc()
test_cmp_array()
test_cmp_heir()
test_cmp_willexecutor()
test_search_heir_by_values()
test_cmp_heir_by_values()
test_cmp_heirs_by_values()
test_cmp_inputs()
test_cmp_outputs()
test_cmp_txs()
test_get_value_amount()
test_chk_locktime()
test_anticipate_locktime()
test_cmp_locktime()
test_get_locktimes()
test_get_lowest_locktimes()
test_get_will_spent_utxos()
test_utxo_to_str()
test_cmp_utxo()
test_in_utxo()
test_cmp_output()
test_in_output()
test_din_output()
test_get_current_height()
test_copy()
test_fix_will_settings_tx_fees()
test_fix_will_tx_fees()
test_text_hex_conversion()
print(f"[OK] All {sum(1 for k in dir() if k.startswith('test_'))} Util tests passed")