diff --git a/doc/HACKING.md b/doc/HACKING.md index 13e3ec4cf..9cf984a6f 100644 --- a/doc/HACKING.md +++ b/doc/HACKING.md @@ -203,9 +203,9 @@ There are three kinds of tests: * **blackbox tests** - These test setup a mini-regtest environment and test lightningd as a whole. They can be run individually: - `PYTHONPATH=contrib/pylightning python3 tests/test_lightningd.py -f`. + `PYTHONPATH=contrib/pylightning py.test -v tests/`. - You can also append `LightningDTests.TESTNAME` to run a single test. + You can also append `-k TESTNAME` to run a single test. Our Travis CI instance (see `.travis.yml`) runs all these for each pull request. diff --git a/tests/benchmark.py b/tests/benchmark.py index 971eee87d..18c756794 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -1,12 +1,14 @@ +from concurrent import futures +from fixtures import * # noqa: F401,F403 +from time import time +from tqdm import tqdm + + import logging import pytest import random import utils -from concurrent import futures -from test_lightningd import NodeFactory -from time import time -from tqdm import tqdm num_workers = 480 num_payments = 10000 @@ -38,13 +40,6 @@ def bitcoind(): bitcoind.proc.wait() -@pytest.fixture -def node_factory(request, bitcoind, executor): - nf = NodeFactory(request.node.name, bitcoind, executor) - yield nf - nf.killall([False] * len(nf.nodes)) - - def test_single_hop(node_factory, executor): l1 = node_factory.get_node() l2 = node_factory.get_node() diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 050370ef3..cc2bc8e03 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -1,5 +1,5 @@ from fixtures import * # noqa: F401,F403 -from test_lightningd import wait_for +from utils import wait_for import json import logging diff --git a/tests/test_lightningd.py b/tests/test_lightningd.py deleted file mode 100644 index 99507db24..000000000 --- a/tests/test_lightningd.py +++ /dev/null @@ -1,272 +0,0 @@ -from concurrent import futures -from decimal import Decimal -from flaky import flaky -from utils import NodeFactory, wait_for, only_one - -import copy -import json -import logging -import os -import random -import re -import shutil -import socket -import sqlite3 -import string -import subprocess -import sys -import tempfile -import time -import unittest - -import utils -from lightning import RpcError - -with open('config.vars') as configfile: - config = dict([(line.rstrip().split('=', 1)) for line in configfile]) - -bitcoind = None -TEST_DIR = tempfile.mkdtemp(prefix='lightning-') -VALGRIND = os.getenv("VALGRIND", config['VALGRIND']) == "1" -DEVELOPER = os.getenv("DEVELOPER", config['DEVELOPER']) == "1" -TEST_DEBUG = os.getenv("TEST_DEBUG", "0") == "1" - - -if TEST_DEBUG: - logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) -logging.info("Tests running in '%s'", TEST_DIR) - - -def to_json(arg): - return json.loads(json.dumps(arg)) - - -def setupBitcoind(directory): - global bitcoind - bitcoind = utils.BitcoinD(bitcoin_dir=directory, rpcport=None) - - try: - bitcoind.start() - except Exception: - teardown_bitcoind() - raise - - info = bitcoind.rpc.getnetworkinfo() - - if info['version'] < 160000: - bitcoind.rpc.stop() - raise ValueError("bitcoind is too old. At least version 16000 (v0.16.0)" - " is needed, current version is {}".format(info['version'])) - - info = bitcoind.rpc.getblockchaininfo() - # Make sure we have some spendable funds - if info['blocks'] < 101: - bitcoind.generate_block(101 - info['blocks']) - elif bitcoind.rpc.getwalletinfo()['balance'] < 1: - logging.debug("Insufficient balance, generating 1 block") - bitcoind.generate_block(1) - - -def wait_forget_channels(node): - """This node is closing all of its channels, check we are forgetting them - """ - node.daemon.wait_for_log(r'onchaind complete, forgetting peer') - # May have reconnected, but should merely be gossiping. - for peer in node.rpc.listpeers()['peers']: - assert peer['state'] == 'GOSSIPING' - assert node.db_query("SELECT * FROM channels") == [] - - -def sync_blockheight(nodes): - target = nodes[0].bitcoin.rpc.getblockcount() - for n in nodes: - wait_for(lambda: n.rpc.getinfo()['blockheight'] == target) - - -def teardown_bitcoind(): - global bitcoind - try: - bitcoind.rpc.stop() - except Exception: - bitcoind.proc.kill() - bitcoind.proc.wait() - - -class BaseLightningDTests(unittest.TestCase): - def setUp(self): - bitcoin_dir = os.path.join(TEST_DIR, self._testMethodName, "bitcoind") - setupBitcoind(bitcoin_dir) - # Most of the executor threads will be waiting for IO, so - # let's have a few of them - self.executor = futures.ThreadPoolExecutor(max_workers=20) - self.node_factory = NodeFactory(self._testMethodName, bitcoind, self.executor, directory=TEST_DIR) - - def getValgrindErrors(self, node): - for error_file in os.listdir(node.daemon.lightning_dir): - if not re.fullmatch("valgrind-errors.\d+", error_file): - continue - with open(os.path.join(node.daemon.lightning_dir, error_file), 'r') as f: - errors = f.read().strip() - if errors: - return errors, error_file - return None, None - - def printValgrindErrors(self, node): - errors, fname = self.getValgrindErrors(node) - if errors: - print("-" * 31, "Valgrind errors", "-" * 32) - print("Valgrind error file:", fname) - print(errors) - print("-" * 80) - return 1 if errors else 0 - - def getCrashLog(self, node): - if node.may_fail: - return None, None - try: - crashlog = os.path.join(node.daemon.lightning_dir, 'crash.log') - with open(crashlog, 'r') as f: - return f.readlines(), crashlog - except Exception: - return None, None - - def printCrashLog(self, node): - errors, fname = self.getCrashLog(node) - if errors: - print("-" * 10, "{} (last 50 lines)".format(fname), "-" * 10) - for l in errors[-50:]: - print(l, end='') - print("-" * 80) - return 1 if errors else 0 - - def checkReconnect(self, node): - # Without DEVELOPER, we can't suppress reconnection. - if node.may_reconnect or not DEVELOPER: - return 0 - if node.daemon.is_in_log('Peer has reconnected'): - return 1 - return 0 - - def checkBadGossipOrder(self, node): - # We can have a race where we notice a channel deleted and someone - # sends an update, and we can get unknown channel updates in errors. - if node.daemon.is_in_log('Bad gossip order from (?!error)') and not node.daemon.is_in_log('Deleting channel'): - return 1 - return 0 - - def tearDown(self): - ok = self.node_factory.killall([not n.may_fail for n in self.node_factory.nodes]) - self.executor.shutdown(wait=False) - - teardown_bitcoind() - err_count = 0 - # Do not check for valgrind error files if it is disabled - if VALGRIND: - for node in self.node_factory.nodes: - err_count += self.printValgrindErrors(node) - if err_count: - raise ValueError("{} nodes reported valgrind errors".format(err_count)) - - for node in self.node_factory.nodes: - err_count += self.printCrashLog(node) - if err_count: - raise ValueError("{} nodes had crash.log files".format(err_count)) - - for node in self.node_factory.nodes: - err_count += self.checkReconnect(node) - if err_count: - raise ValueError("{} nodes had unexpected reconnections".format(err_count)) - - for node in self.node_factory.nodes: - err_count += self.checkBadGossipOrder(node) - if err_count: - raise ValueError("{} nodes had bad gossip order".format(err_count)) - - if not ok: - raise Exception("At least one lightning exited with unexpected non-zero return code") - - shutil.rmtree(self.node_factory.directory) - - -class LightningDTests(BaseLightningDTests): - def connect(self, may_reconnect=False): - l1, l2 = self.node_factory.get_nodes(2, opts={'may_reconnect': may_reconnect}) - ret = l1.rpc.connect(l2.info['id'], 'localhost', l2.port) - - assert ret['id'] == l2.info['id'] - - l1.daemon.wait_for_log('Handing back peer .* to master') - l2.daemon.wait_for_log('Handing back peer .* to master') - return l1, l2 - - # Waits until l1 notices funds - def give_funds(self, l1, satoshi): - addr = l1.rpc.newaddr()['address'] - bitcoind.rpc.sendtoaddress(addr, satoshi / 10**8) - - numfunds = len(l1.rpc.listfunds()['outputs']) - bitcoind.generate_block(1) - wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > numfunds) - - # Returns the short channel-id: :: - def fund_channel(self, l1, l2, amount): - return l1.fund_channel(l2, amount) - - def pay(self, lsrc, ldst, amt, label=None, async=False): - if not label: - label = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20)) - - rhash = ldst.rpc.invoice(amt, label, label)['payment_hash'] - assert only_one(ldst.rpc.listinvoices(label)['invoices'])['status'] == 'unpaid' - - routestep = { - 'msatoshi': amt, - 'id': ldst.info['id'], - 'delay': 5, - 'channel': '1:1:1' - } - - def wait_pay(): - # Up to 10 seconds for payment to succeed. - start_time = time.time() - while only_one(ldst.rpc.listinvoices(label)['invoices'])['status'] != 'paid': - if time.time() > start_time + 10: - raise TimeoutError('Payment timed out') - time.sleep(0.1) - # sendpay is async now - lsrc.rpc.sendpay(to_json([routestep]), rhash) - if async: - return self.executor.submit(wait_pay) - else: - # wait for sendpay to comply - lsrc.rpc.waitsendpay(rhash) - - # This waits until gossipd sees channel_update in both directions - # (or for local channels, at least a local announcement) - def wait_for_routes(self, l1, channel_ids): - bitcoind.generate_block(5) - # Could happen in any order... - l1.daemon.wait_for_logs(['Received channel_update for channel {}\\(0\\)'.format(c) - for c in channel_ids] + - ['Received channel_update for channel {}\\(1\\)'.format(c) - for c in channel_ids]) - - def fake_bitcoind_fail(self, l1, exitcode): - # Create and rename, for atomicity. - f = os.path.join(l1.daemon.lightning_dir, "bitcoin-cli-fail.tmp") - with open(f, "w") as text_file: - print(exitcode, file=text_file) - os.rename(f, os.path.join(l1.daemon.lightning_dir, "bitcoin-cli-fail")) - - def fake_bitcoind_unfail(self, l1): - os.remove(os.path.join(l1.daemon.lightning_dir, "bitcoin-cli-fail")) - - def test_features(self): - l1, l2 = self.connect() - - # LOCAL_INITIAL_ROUTING_SYNC + LOCAL_GOSSIP_QUERIES - assert only_one(l1.rpc.listpeers()['peers'])['local_features'] == '88' - - -if __name__ == '__main__': - unittest.main(verbosity=2)