Christian Decker
7 years ago
committed by
Rusty Russell
4 changed files with 9 additions and 286 deletions
@ -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: <blocknum>:<txnum>:<outnum> |
|||
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) |
Loading…
Reference in new issue