diff --git a/tests/test_sling.py b/tests/test_sling.py new file mode 100644 index 0000000..c85f91a --- /dev/null +++ b/tests/test_sling.py @@ -0,0 +1,302 @@ +#!/usr/bin/python + +from pyln.testing.fixtures import * +from pyln.testing.utils import sync_blockheight, wait_for, only_one +from pyln.client import RpcError +import os +import pytest +from util import get_plugin +from util import VERSION +import time + + +def test_basic(node_factory, get_plugin): + node = node_factory.get_node( + options={ + 'plugin': get_plugin + } + ) + result = node.rpc.call("sling-version") + assert result is not None + assert isinstance(result, dict) is True + assert "version" in result + assert VERSION in result["version"] + + +def test_options(node_factory, get_plugin): + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-refresh-peers-interval': 2, + 'sling-refresh-aliasmap-interval': 3, + 'sling-refresh-graph-interval': 599, + 'sling-reset-liquidity-interval': 300, + 'sling-depleteuptopercent': 0.33, + 'sling-depleteuptoamount': 100000, + 'sling-maxhops': 4, + 'sling-candidates-min-age': 6, + 'sling-paralleljobs': 4, + 'sling-timeoutpay': 60, + 'sling-max-htlc-count': 5, + 'sling-stats-delete-failures-age': 0, + 'sling-stats-delete-successes-age': 0, + 'sling-stats-delete-failures-size': 0, + 'sling-stats-delete-successes-size': 0, + } + ) + configs = node.rpc.call("listconfigs")["configs"] + assert configs["sling-refresh-peers-interval"]["value_int"] == 2 + assert configs["sling-refresh-aliasmap-interval"]["value_int"] == 3 + assert configs["sling-refresh-graph-interval"]["value_int"] == 599 + assert configs["sling-reset-liquidity-interval"]["value_int"] == 300 + assert configs["sling-depleteuptopercent"]["value_str"] == "0.33" + assert configs["sling-depleteuptoamount"]["value_int"] == 100000 + assert configs["sling-maxhops"]["value_int"] == 4 + assert configs["sling-candidates-min-age"]["value_int"] == 6 + assert configs["sling-paralleljobs"]["value_int"] == 4 + assert configs["sling-timeoutpay"]["value_int"] == 60 + assert configs["sling-max-htlc-count"]["value_int"] == 5 + assert configs["sling-stats-delete-failures-age"]["value_int"] == 0 + assert configs["sling-stats-delete-successes-age"]["value_int"] == 0 + assert configs["sling-stats-delete-failures-size"]["value_int"] == 0 + assert configs["sling-stats-delete-successes-size"]["value_int"] == 0 + + +def test_option_errors(node_factory, get_plugin): + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-refresh-peers-interval': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-refresh-peers-interval must be ' + r'greater than or equal to 1')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-refresh-aliasmap-interval': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-refresh-aliasmap-interval must be ' + r'greater than or equal to 1')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-refresh-graph-interval': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-refresh-graph-interval must be ' + r'greater than or equal to 1')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-reset-liquidity-interval': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-reset-liquidity-interval must be ' + r'greater than or equal to 1')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-depleteuptopercent': 1, + } + ) + assert node.daemon.is_in_log( + r'sling-depleteuptopercent needs to be greater than 0 and <1') + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-depleteuptoamount': -10, + } + ) + assert node.daemon.is_in_log( + (r'sling-depleteuptoamount needs to be a positive number')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-maxhops': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-maxhops must be ' + r'greater than or equal to 2')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-candidates-min-age': -10, + } + ) + assert node.daemon.is_in_log( + r'sling-candidates-min-age needs to be a positive number') + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-paralleljobs': 0, + } + ) + assert node.daemon.is_in_log( + (r'sling-paralleljobs must be ' + r'greater than or equal to 1')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-timeoutpay': 0, + } + ) + assert node.daemon.is_in_log( + r'sling-timeoutpay must be greater than or equal to 1') + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-max-htlc-count': 0, + } + ) + assert node.daemon.is_in_log( + r'sling-max-htlc-count must be greater than or equal to 1') + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-stats-delete-failures-age': 99999999, + } + ) + assert node.daemon.is_in_log( + (r'sling-stats-delete-failures-age needs to be ' + r'a positive number and smaller than')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-stats-delete-successes-age': 99999999, + } + ) + assert node.daemon.is_in_log( + (r'sling-stats-delete-successes-age needs to be ' + r'a positive number and smaller than')) + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-stats-delete-failures-size': -10, + } + ) + assert node.daemon.is_in_log( + r'sling-stats-delete-failures-size needs to be a positive number') + + node = node_factory.get_node( + options={ + 'plugin': get_plugin, + 'sling-stats-delete-successes-size': -10, + } + ) + assert node.daemon.is_in_log( + r'sling-stats-delete-successes-size needs to be a positive number') + + +def test_maxhops_2(node_factory, bitcoind, get_plugin): + l1, l2 = node_factory.get_nodes(2, + opts={ + 'plugin': get_plugin, + 'sling-maxhops': 2, + 'sling-refresh-graph-interval': 1 + } + ) + l1.fundwallet(10_000_000) + l2.fundwallet(10_000_000) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.rpc.fundchannel(l2.info['id'], 1_000_000, mindepth=1) + l2.rpc.fundchannel(l1.info['id'], 1_000_000, mindepth=1) + + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [l1, l2]) + + chans = l2.rpc.listpeerchannels(l1.info['id'])["channels"] + for chan in chans: + if chan["opener"] == "local": + cl2 = chan["short_channel_id"] + else: + cl1 = chan["short_channel_id"] + l2.wait_channel_active(cl1) + l2.wait_channel_active(cl2) + + # wait for plugin gossip refresh + time.sleep(2) + + l1.rpc.call( + "sling-job", {"scid": cl2, "direction": "pull", + "amount": 100_000, "maxppm": 1000, "outppm": 1000}) + l1.rpc.call("sling-go", []) + l1.daemon.wait_for_log(r"already balanced. Taking a break") + wait_for(lambda: l1.rpc.listpeerchannels( + l2.info['id'])['channels'][0]['to_us_msat'] >= 400_000_000) + wait_for(lambda: l1.rpc.listpeerchannels( + l2.info['id'])['channels'][0]['to_us_msat'] <= 600_000_000) + + +def test_pull_and_push(node_factory, bitcoind, get_plugin): + l1, l2, l3 = node_factory.get_nodes(3, + opts={ + 'plugin': get_plugin, + 'sling-refresh-graph-interval': 1 + } + ) + l1.fundwallet(10_000_000) + l2.fundwallet(10_000_000) + l3.fundwallet(10_000_000) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + l3.rpc.connect(l1.info['id'], 'localhost', l1.port) + l1.rpc.fundchannel(l2.info['id'], 1_000_000, mindepth=1, announce=True) + l2.rpc.fundchannel(l3.info['id'], 1_000_000, mindepth=1, announce=True) + l3.rpc.fundchannel(l1.info['id'], 1_000_000, mindepth=1, announce=True) + + bitcoind.generate_block(6) + sync_blockheight(bitcoind, [l1, l2, l3]) + + cl1 = l1.rpc.listpeerchannels(l2.info['id'])[ + "channels"][0]["short_channel_id"] + cl2 = l2.rpc.listpeerchannels(l3.info['id'])[ + "channels"][0]["short_channel_id"] + cl3 = l3.rpc.listpeerchannels(l1.info['id'])[ + "channels"][0]["short_channel_id"] + + for n in [l1, l2, l3]: + for scid in [cl1, cl2, cl3]: + n.wait_channel_active(scid) + + # wait for plugin gossip refresh + time.sleep(2) + + l1.rpc.call( + "sling-job", {"scid": cl3, "direction": "pull", + "amount": 100_000, "maxppm": 1000, "outppm": 1000}) + l1.rpc.call("sling-go", []) + l1.daemon.wait_for_log(r"already balanced. Taking a break") + wait_for(lambda: only_one(l1.rpc.listpeerchannels( + l3.info['id'])['channels'])['to_us_msat'] >= 400_000_000) + wait_for(lambda: only_one(l1.rpc.listpeerchannels( + l3.info['id'])['channels'])['to_us_msat'] <= 600_000_000) + + l1.rpc.call("sling-deletejob", ["all"]) + l1.rpc.call( + "sling-job", {"scid": cl3, "direction": "push", "target": 1, + "amount": 100_000, "maxppm": 1000, "outppm": 0, + "depleteuptopercent": 0}) + l1.rpc.call("sling-go", []) + l1.daemon.wait_for_log(r"already balanced. Taking a break") + wait_for(lambda: only_one(l1.rpc.listpeerchannels( + l3.info['id'])['channels'])['to_us_msat'] <= 120_000_000) diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..626841d --- /dev/null +++ b/tests/util.py @@ -0,0 +1,73 @@ +import string +import random +import logging +import os +import pytest +from pathlib import Path +import requests +import tarfile +import platform + +VERSION = "v1.4.1" +RUST_PROFILE = os.environ.get("RUST_PROFILE", "debug") +COMPILED_PATH = Path.cwd() / "target" / RUST_PROFILE / \ + "sling" + + +@pytest.fixture +def get_plugin(directory): + downloaded_plugin_path = Path( + os.path.join(directory, "sling.tar.gz")) + extracted_plugin_path = Path(os.path.join(directory, "sling")) + if COMPILED_PATH.is_file(): + return COMPILED_PATH + elif extracted_plugin_path.is_file(): + return extracted_plugin_path + else: + architecture = get_architecture() + + url = (f"https://github.com/daywalker90/sling/releases/download/" + f"{VERSION}/sling-{VERSION}-{architecture}.tar.gz") + response = requests.get(url) + with open(downloaded_plugin_path, "wb") as file: + file.write(response.content) + + with tarfile.open(downloaded_plugin_path, "r:gz") as tar: + tar.extractall(directory) + + return extracted_plugin_path + + +def get_architecture(): + machine = platform.machine() + + if machine == 'x86_64': + return 'x86_64-linux-gnu' + elif machine == 'armv7l': + return 'armv7-linux-gnueabihf' + elif machine == 'aarch64': + return 'aarch64-linux-gnu' + else: + raise RuntimeError( + f"No self-compiled binary found and " + f"unsupported release-architecture: {machine}") + + +def generate_random_label(): + label_length = 8 + random_label = ''.join(random.choice(string.ascii_letters) + for _ in range(label_length)) + return random_label + + +def generate_random_number(): + return random.randint(1, 20_000_000_000_000_00_000) + + +def pay_with_thread(rpc, bolt11): + LOGGER = logging.getLogger(__name__) + try: + rpc.dev_pay(bolt11, dev_use_shadow=False) + except Exception as e: + LOGGER.debug(f"holdinvoice: Error paying payment hash:{e}") + pass