""" 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")