mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 15:14:23 +01:00
pyln-testing: use files for stdout and stderr, not threads.
Some flakes are caused by weird races in this code. Plus, if we get things to write straight to files, we might see things in there on post-mortem which happen after the python runner exits. It's a bit less efficient, but much simpler. Let's see if it helps! Some tests need a rework now, since we don't get a failure (except eventual timeout), but they're simpler. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
@@ -174,13 +174,20 @@ class TailableProc(object):
|
|||||||
tail the processes and react to their output.
|
tail the processes and react to their output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, outputDir=None, verbose=True):
|
def __init__(self, outputDir, verbose=True):
|
||||||
self.logs = []
|
self.logs = []
|
||||||
self.logs_cond = threading.Condition(threading.RLock())
|
|
||||||
self.env = os.environ.copy()
|
self.env = os.environ.copy()
|
||||||
self.running = False
|
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self.outputDir = outputDir
|
self.outputDir = outputDir
|
||||||
|
if not os.path.exists(outputDir):
|
||||||
|
os.makedirs(outputDir)
|
||||||
|
# Create and open them.
|
||||||
|
self.stdout_filename = os.path.join(outputDir, "log")
|
||||||
|
self.stderr_filename = os.path.join(outputDir, "errlog")
|
||||||
|
self.stdout_write = open(self.stdout_filename, "wt")
|
||||||
|
self.stderr_write = open(self.stderr_filename, "wt")
|
||||||
|
self.stdout_read = open(self.stdout_filename, "rt")
|
||||||
|
self.stderr_read = open(self.stderr_filename, "rt")
|
||||||
self.logsearch_start = 0
|
self.logsearch_start = 0
|
||||||
self.err_logs = []
|
self.err_logs = []
|
||||||
self.prefix = ""
|
self.prefix = ""
|
||||||
@@ -192,29 +199,17 @@ class TailableProc(object):
|
|||||||
# pass it to the log matcher and not print it to stdout).
|
# pass it to the log matcher and not print it to stdout).
|
||||||
self.log_filter = lambda line: False
|
self.log_filter = lambda line: False
|
||||||
|
|
||||||
def start(self, stdin=None, stdout=None, stderr=None):
|
def start(self, stdin=None):
|
||||||
"""Start the underlying process and start monitoring it.
|
"""Start the underlying process and start monitoring it.
|
||||||
"""
|
"""
|
||||||
logging.debug("Starting '%s'", " ".join(self.cmd_line))
|
logging.debug("Starting '%s'", " ".join(self.cmd_line))
|
||||||
self.proc = subprocess.Popen(self.cmd_line,
|
self.proc = subprocess.Popen(self.cmd_line,
|
||||||
stdin=stdin,
|
stdin=stdin,
|
||||||
stdout=stdout if stdout else subprocess.PIPE,
|
stdout=self.stdout_write,
|
||||||
stderr=stderr,
|
stderr=self.stderr_write,
|
||||||
env=self.env)
|
env=self.env)
|
||||||
self.thread = threading.Thread(target=self.tail)
|
|
||||||
self.thread.daemon = True
|
|
||||||
self.thread.start()
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def save_log(self):
|
|
||||||
if self.outputDir:
|
|
||||||
logpath = os.path.join(self.outputDir, 'log')
|
|
||||||
with open(logpath, 'w') as f:
|
|
||||||
for l in self.logs:
|
|
||||||
f.write(l + '\n')
|
|
||||||
|
|
||||||
def stop(self, timeout=10):
|
def stop(self, timeout=10):
|
||||||
self.save_log()
|
|
||||||
self.proc.terminate()
|
self.proc.terminate()
|
||||||
|
|
||||||
# Now give it some time to react to the signal
|
# Now give it some time to react to the signal
|
||||||
@@ -224,56 +219,32 @@ class TailableProc(object):
|
|||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
|
|
||||||
self.proc.wait()
|
self.proc.wait()
|
||||||
self.thread.join()
|
|
||||||
|
|
||||||
return self.proc.returncode
|
return self.proc.returncode
|
||||||
|
|
||||||
def kill(self):
|
def kill(self):
|
||||||
"""Kill process without giving it warning."""
|
"""Kill process without giving it warning."""
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
self.proc.wait()
|
self.proc.wait()
|
||||||
self.thread.join()
|
|
||||||
|
|
||||||
def tail(self):
|
def logs_catchup(self):
|
||||||
"""Tail the stdout of the process and remember it.
|
"""Save the latest stdout / stderr contents; return true if we got anything.
|
||||||
|
|
||||||
Stores the lines of output produced by the process in
|
|
||||||
self.logs and signals that a new line was read so that it can
|
|
||||||
be picked up by consumers.
|
|
||||||
"""
|
"""
|
||||||
for line in iter(self.proc.stdout.readline, ''):
|
new_stdout = self.stdout_read.readlines()
|
||||||
if len(line) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
line = line.decode('UTF-8', 'replace').rstrip()
|
|
||||||
|
|
||||||
if self.log_filter(line):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
sys.stdout.write("{}: {}\n".format(self.prefix, line))
|
for line in new_stdout:
|
||||||
|
sys.stdout.write("{}: {}".format(self.prefix, line))
|
||||||
with self.logs_cond:
|
self.logs += [l.rstrip() for l in new_stdout]
|
||||||
self.logs.append(line)
|
new_stderr = self.stderr_read.readlines()
|
||||||
self.logs_cond.notifyAll()
|
if self.verbose:
|
||||||
|
for line in new_stderr:
|
||||||
self.running = False
|
sys.stderr.write("{}-stderr: {}".format(self.prefix, line))
|
||||||
self.proc.stdout.close()
|
self.err_logs += [l.rstrip() for l in new_stderr]
|
||||||
|
return len(new_stdout) > 0 or len(new_stderr) > 0
|
||||||
if self.proc.stderr:
|
|
||||||
for line in iter(self.proc.stderr.readline, ''):
|
|
||||||
|
|
||||||
if line is None or len(line) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
line = line.rstrip().decode('UTF-8', 'replace')
|
|
||||||
self.err_logs.append(line)
|
|
||||||
|
|
||||||
self.proc.stderr.close()
|
|
||||||
|
|
||||||
def is_in_log(self, regex, start=0):
|
def is_in_log(self, regex, start=0):
|
||||||
"""Look for `regex` in the logs."""
|
"""Look for `regex` in the logs."""
|
||||||
|
|
||||||
|
self.logs_catchup()
|
||||||
ex = re.compile(regex)
|
ex = re.compile(regex)
|
||||||
for l in self.logs[start:]:
|
for l in self.logs[start:]:
|
||||||
if ex.search(l):
|
if ex.search(l):
|
||||||
@@ -286,6 +257,7 @@ class TailableProc(object):
|
|||||||
def is_in_stderr(self, regex):
|
def is_in_stderr(self, regex):
|
||||||
"""Look for `regex` in stderr."""
|
"""Look for `regex` in stderr."""
|
||||||
|
|
||||||
|
self.logs_catchup()
|
||||||
ex = re.compile(regex)
|
ex = re.compile(regex)
|
||||||
for l in self.err_logs:
|
for l in self.err_logs:
|
||||||
if ex.search(l):
|
if ex.search(l):
|
||||||
@@ -311,31 +283,29 @@ class TailableProc(object):
|
|||||||
logging.debug("Waiting for {} in the logs".format(regexs))
|
logging.debug("Waiting for {} in the logs".format(regexs))
|
||||||
exs = [re.compile(r) for r in regexs]
|
exs = [re.compile(r) for r in regexs]
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
pos = self.logsearch_start
|
|
||||||
while True:
|
while True:
|
||||||
|
if self.logsearch_start >= len(self.logs):
|
||||||
|
if not self.logs_catchup():
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
if timeout is not None and time.time() > start_time + timeout:
|
if timeout is not None and time.time() > start_time + timeout:
|
||||||
print("Time-out: can't find {} in logs".format(exs))
|
print("Time-out: can't find {} in logs".format(exs))
|
||||||
for r in exs:
|
for r in exs:
|
||||||
if self.is_in_log(r):
|
if self.is_in_log(r):
|
||||||
print("({} was previously in logs!)".format(r))
|
print("({} was previously in logs!)".format(r))
|
||||||
raise TimeoutError('Unable to find "{}" in logs.'.format(exs))
|
raise TimeoutError('Unable to find "{}" in logs.'.format(exs))
|
||||||
|
|
||||||
with self.logs_cond:
|
|
||||||
if pos >= len(self.logs):
|
|
||||||
if not self.running:
|
|
||||||
raise ValueError('Process died while waiting for logs')
|
|
||||||
self.logs_cond.wait(1)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
line = self.logs[self.logsearch_start]
|
||||||
|
self.logsearch_start += 1
|
||||||
for r in exs.copy():
|
for r in exs.copy():
|
||||||
self.logsearch_start = pos + 1
|
if r.search(line):
|
||||||
if r.search(self.logs[pos]):
|
|
||||||
logging.debug("Found '%s' in logs", r)
|
logging.debug("Found '%s' in logs", r)
|
||||||
exs.remove(r)
|
exs.remove(r)
|
||||||
break
|
|
||||||
if len(exs) == 0:
|
if len(exs) == 0:
|
||||||
return self.logs[pos]
|
return line
|
||||||
pos += 1
|
# Don't match same line with different regexs!
|
||||||
|
break
|
||||||
|
|
||||||
def wait_for_log(self, regex, timeout=TIMEOUT):
|
def wait_for_log(self, regex, timeout=TIMEOUT):
|
||||||
"""Look for `regex` in the logs.
|
"""Look for `regex` in the logs.
|
||||||
@@ -620,10 +590,9 @@ class LightningD(TailableProc):
|
|||||||
|
|
||||||
return self.cmd_prefix + [self.executable] + opts
|
return self.cmd_prefix + [self.executable] + opts
|
||||||
|
|
||||||
def start(self, stdin=None, stdout=None, stderr=None,
|
def start(self, stdin=None, wait_for_initialized=True):
|
||||||
wait_for_initialized=True):
|
|
||||||
self.opts['bitcoin-rpcport'] = self.rpcproxy.rpcport
|
self.opts['bitcoin-rpcport'] = self.rpcproxy.rpcport
|
||||||
TailableProc.start(self, stdin, stdout, stderr)
|
TailableProc.start(self, stdin)
|
||||||
if wait_for_initialized:
|
if wait_for_initialized:
|
||||||
self.wait_for_log("Server started with public key")
|
self.wait_for_log("Server started with public key")
|
||||||
logging.info("LightningD started")
|
logging.info("LightningD started")
|
||||||
@@ -852,8 +821,8 @@ class LightningNode(object):
|
|||||||
info = self.rpc.getinfo()
|
info = self.rpc.getinfo()
|
||||||
return 'warning_bitcoind_sync' not in info and 'warning_lightningd_sync' not in info
|
return 'warning_bitcoind_sync' not in info and 'warning_lightningd_sync' not in info
|
||||||
|
|
||||||
def start(self, wait_for_bitcoind_sync=True, stderr=None):
|
def start(self, wait_for_bitcoind_sync=True):
|
||||||
self.daemon.start(stderr=stderr)
|
self.daemon.start()
|
||||||
# Cache `getinfo`, we'll be using it a lot
|
# Cache `getinfo`, we'll be using it a lot
|
||||||
self.info = self.rpc.getinfo()
|
self.info = self.rpc.getinfo()
|
||||||
# This shortcut is sufficient for our simple tests.
|
# This shortcut is sufficient for our simple tests.
|
||||||
@@ -878,7 +847,6 @@ class LightningNode(object):
|
|||||||
if self.rc is None:
|
if self.rc is None:
|
||||||
self.rc = self.daemon.stop()
|
self.rc = self.daemon.stop()
|
||||||
|
|
||||||
self.daemon.save_log()
|
|
||||||
self.daemon.cleanup()
|
self.daemon.cleanup()
|
||||||
|
|
||||||
if self.rc != 0 and not self.may_fail:
|
if self.rc != 0 and not self.may_fail:
|
||||||
@@ -1417,12 +1385,7 @@ class NodeFactory(object):
|
|||||||
|
|
||||||
if start:
|
if start:
|
||||||
try:
|
try:
|
||||||
# Capture stderr if we're failing
|
node.start(wait_for_bitcoind_sync)
|
||||||
if expect_fail:
|
|
||||||
stderr = subprocess.PIPE
|
|
||||||
else:
|
|
||||||
stderr = None
|
|
||||||
node.start(wait_for_bitcoind_sync, stderr=stderr)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
if expect_fail:
|
if expect_fail:
|
||||||
return node
|
return node
|
||||||
|
|||||||
@@ -109,13 +109,13 @@ def test_bitcoin_failure(node_factory, bitcoind):
|
|||||||
# Ignore BROKEN log message about blocksonly mode.
|
# Ignore BROKEN log message about blocksonly mode.
|
||||||
l2 = node_factory.get_node(start=False, expect_fail=True,
|
l2 = node_factory.get_node(start=False, expect_fail=True,
|
||||||
allow_broken_log=True)
|
allow_broken_log=True)
|
||||||
with pytest.raises(ValueError):
|
l2.daemon.start(wait_for_initialized=False)
|
||||||
l2.start(stderr=subprocess.PIPE)
|
# Will exit with failure code.
|
||||||
|
assert l2.daemon.wait() == 1
|
||||||
assert l2.daemon.is_in_stderr(r".*deactivating transaction relay is not"
|
assert l2.daemon.is_in_stderr(r".*deactivating transaction relay is not"
|
||||||
" supported.") is not None
|
" supported.")
|
||||||
# wait_for_log gets upset since daemon is not running.
|
assert l2.daemon.is_in_log('deactivating transaction'
|
||||||
wait_for(lambda: l2.daemon.is_in_log('deactivating transaction'
|
' relay is not supported')
|
||||||
' relay is not supported'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_bitcoin_ibd(node_factory, bitcoind):
|
def test_bitcoin_ibd(node_factory, bitcoind):
|
||||||
@@ -1207,8 +1207,10 @@ def test_rescan(node_factory, bitcoind):
|
|||||||
l1.daemon.opts['rescan'] = -500000
|
l1.daemon.opts['rescan'] = -500000
|
||||||
l1.stop()
|
l1.stop()
|
||||||
bitcoind.generate_block(4)
|
bitcoind.generate_block(4)
|
||||||
with pytest.raises(ValueError):
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
l1.start()
|
# Will exit with failure code.
|
||||||
|
assert l1.daemon.wait() == 1
|
||||||
|
assert l1.daemon.is_in_stderr(r"bitcoind has gone backwards from 500000 to 105 blocks!")
|
||||||
|
|
||||||
# Restarting with future absolute blockheight is fine if we can find it.
|
# Restarting with future absolute blockheight is fine if we can find it.
|
||||||
l1.daemon.opts['rescan'] = -105
|
l1.daemon.opts['rescan'] = -105
|
||||||
@@ -1236,14 +1238,19 @@ def test_bitcoind_goes_backwards(node_factory, bitcoind):
|
|||||||
bitcoind.start()
|
bitcoind.start()
|
||||||
|
|
||||||
# Will simply refuse to start.
|
# Will simply refuse to start.
|
||||||
with pytest.raises(ValueError):
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
l1.start()
|
# Will exit with failure code.
|
||||||
|
assert l1.daemon.wait() == 1
|
||||||
|
assert l1.daemon.is_in_stderr('bitcoind has gone backwards')
|
||||||
|
|
||||||
# Nor will it start with if we ask for a reindex of fewer blocks.
|
# Nor will it start with if we ask for a reindex of fewer blocks.
|
||||||
l1.daemon.opts['rescan'] = 3
|
l1.daemon.opts['rescan'] = 3
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
# Will simply refuse to start.
|
||||||
l1.start()
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
|
# Will exit with failure code.
|
||||||
|
assert l1.daemon.wait() == 1
|
||||||
|
assert l1.daemon.is_in_stderr('bitcoind has gone backwards')
|
||||||
|
|
||||||
# This will force it, however.
|
# This will force it, however.
|
||||||
l1.daemon.opts['rescan'] = -100
|
l1.daemon.opts['rescan'] = -100
|
||||||
@@ -1690,7 +1697,7 @@ def test_newaddr(node_factory, chainparams):
|
|||||||
assert both['bech32'].startswith(chainparams['bip173_prefix'])
|
assert both['bech32'].startswith(chainparams['bip173_prefix'])
|
||||||
|
|
||||||
|
|
||||||
def test_bitcoind_fail_first(node_factory, bitcoind, executor):
|
def test_bitcoind_fail_first(node_factory, bitcoind):
|
||||||
"""Make sure we handle spurious bitcoin-cli failures during startup
|
"""Make sure we handle spurious bitcoin-cli failures during startup
|
||||||
|
|
||||||
See [#2687](https://github.com/ElementsProject/lightning/issues/2687) for
|
See [#2687](https://github.com/ElementsProject/lightning/issues/2687) for
|
||||||
@@ -1699,7 +1706,9 @@ def test_bitcoind_fail_first(node_factory, bitcoind, executor):
|
|||||||
"""
|
"""
|
||||||
# Do not start the lightning node since we need to instrument bitcoind
|
# Do not start the lightning node since we need to instrument bitcoind
|
||||||
# first.
|
# first.
|
||||||
l1 = node_factory.get_node(start=False)
|
l1 = node_factory.get_node(start=False,
|
||||||
|
allow_broken_log=True,
|
||||||
|
may_fail=True)
|
||||||
|
|
||||||
# Instrument bitcoind to fail some queries first.
|
# Instrument bitcoind to fail some queries first.
|
||||||
def mock_fail(*args):
|
def mock_fail(*args):
|
||||||
@@ -1708,22 +1717,17 @@ def test_bitcoind_fail_first(node_factory, bitcoind, executor):
|
|||||||
l1.daemon.rpcproxy.mock_rpc('getblockhash', mock_fail)
|
l1.daemon.rpcproxy.mock_rpc('getblockhash', mock_fail)
|
||||||
l1.daemon.rpcproxy.mock_rpc('estimatesmartfee', mock_fail)
|
l1.daemon.rpcproxy.mock_rpc('estimatesmartfee', mock_fail)
|
||||||
|
|
||||||
f = executor.submit(l1.start)
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
|
l1.daemon.wait_for_logs([r'getblockhash [a-z0-9]* exited with status 1',
|
||||||
wait_for(lambda: l1.daemon.running)
|
r'Unable to estimate opening fees',
|
||||||
# Make sure it fails on the first `getblock` call (need to use `is_in_log`
|
r'BROKEN.*we have been retrying command for --bitcoin-retry-timeout=60 seconds'])
|
||||||
# since the `wait_for_log` in `start` sets the offset)
|
# Will exit with failure code.
|
||||||
wait_for(lambda: l1.daemon.is_in_log(
|
assert l1.daemon.wait() == 1
|
||||||
r'getblockhash [a-z0-9]* exited with status 1'))
|
|
||||||
wait_for(lambda: l1.daemon.is_in_log(
|
|
||||||
r'Unable to estimate opening fees'))
|
|
||||||
|
|
||||||
# Now unset the mock, so calls go through again
|
# Now unset the mock, so calls go through again
|
||||||
l1.daemon.rpcproxy.mock_rpc('getblockhash', None)
|
l1.daemon.rpcproxy.mock_rpc('getblockhash', None)
|
||||||
l1.daemon.rpcproxy.mock_rpc('estimatesmartfee', None)
|
l1.daemon.rpcproxy.mock_rpc('estimatesmartfee', None)
|
||||||
|
|
||||||
f.result()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.developer("needs --dev-force-bip32-seed")
|
@pytest.mark.developer("needs --dev-force-bip32-seed")
|
||||||
@unittest.skipIf(TEST_NETWORK != 'regtest', "Addresses are network specific")
|
@unittest.skipIf(TEST_NETWORK != 'regtest', "Addresses are network specific")
|
||||||
@@ -2077,8 +2081,9 @@ def test_new_node_is_mainnet(node_factory):
|
|||||||
del l1.daemon.opts['network']
|
del l1.daemon.opts['network']
|
||||||
|
|
||||||
# Wrong chain, will fail to start, but that's OK.
|
# Wrong chain, will fail to start, but that's OK.
|
||||||
with pytest.raises(ValueError):
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
l1.start()
|
# Will exit with failure code.
|
||||||
|
assert l1.daemon.wait() == 1
|
||||||
|
|
||||||
# Should create these
|
# Should create these
|
||||||
assert os.path.isfile(os.path.join(netdir, "hsm_secret"))
|
assert os.path.isfile(os.path.join(netdir, "hsm_secret"))
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def test_option_types(node_factory):
|
|||||||
}, expect_fail=True, may_fail=True)
|
}, expect_fail=True, may_fail=True)
|
||||||
|
|
||||||
# the node should fail to start, and we get a stderr msg
|
# the node should fail to start, and we get a stderr msg
|
||||||
assert not n.daemon.running
|
assert n.daemon.wait() == 1
|
||||||
wait_for(lambda: n.daemon.is_in_stderr('bool_opt: ! does not parse as type bool'))
|
wait_for(lambda: n.daemon.is_in_stderr('bool_opt: ! does not parse as type bool'))
|
||||||
|
|
||||||
# What happens if we give it a bad int-option?
|
# What happens if we give it a bad int-option?
|
||||||
@@ -106,7 +106,7 @@ def test_option_types(node_factory):
|
|||||||
}, may_fail=True, expect_fail=True)
|
}, may_fail=True, expect_fail=True)
|
||||||
|
|
||||||
# the node should fail to start, and we get a stderr msg
|
# the node should fail to start, and we get a stderr msg
|
||||||
assert not n.daemon.running
|
assert n.daemon.wait() == 1
|
||||||
assert n.daemon.is_in_stderr('--int_opt: notok does not parse as type int')
|
assert n.daemon.is_in_stderr('--int_opt: notok does not parse as type int')
|
||||||
|
|
||||||
# Flag opts shouldn't allow any input
|
# Flag opts shouldn't allow any input
|
||||||
@@ -119,7 +119,7 @@ def test_option_types(node_factory):
|
|||||||
}, may_fail=True, expect_fail=True)
|
}, may_fail=True, expect_fail=True)
|
||||||
|
|
||||||
# the node should fail to start, and we get a stderr msg
|
# the node should fail to start, and we get a stderr msg
|
||||||
assert not n.daemon.running
|
assert n.daemon.wait() == 1
|
||||||
assert n.daemon.is_in_stderr("--flag_opt: doesn't allow an argument")
|
assert n.daemon.is_in_stderr("--flag_opt: doesn't allow an argument")
|
||||||
|
|
||||||
n = node_factory.get_node(options={
|
n = node_factory.get_node(options={
|
||||||
@@ -1500,9 +1500,10 @@ def test_libplugin(node_factory):
|
|||||||
l1.stop()
|
l1.stop()
|
||||||
l1.daemon.opts["name-deprecated"] = "test_opt"
|
l1.daemon.opts["name-deprecated"] = "test_opt"
|
||||||
|
|
||||||
# This actually dies while waiting for the logs.
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
with pytest.raises(ValueError):
|
# Will exit with failure code.
|
||||||
l1.start()
|
assert l1.daemon.wait() == 1
|
||||||
|
assert l1.daemon.is_in_stderr(r"name-deprecated: deprecated option")
|
||||||
|
|
||||||
del l1.daemon.opts["name-deprecated"]
|
del l1.daemon.opts["name-deprecated"]
|
||||||
l1.start()
|
l1.start()
|
||||||
@@ -1646,24 +1647,20 @@ def test_bitcoin_backend(node_factory, bitcoind):
|
|||||||
# We don't start if we haven't all the required methods registered.
|
# We don't start if we haven't all the required methods registered.
|
||||||
plugin = os.path.join(os.getcwd(), "tests/plugins/bitcoin/part1.py")
|
plugin = os.path.join(os.getcwd(), "tests/plugins/bitcoin/part1.py")
|
||||||
l1.daemon.opts["plugin"] = plugin
|
l1.daemon.opts["plugin"] = plugin
|
||||||
try:
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
l1.daemon.start()
|
l1.daemon.wait_for_log("Missing a Bitcoin plugin command")
|
||||||
except ValueError:
|
# Will exit with failure code.
|
||||||
assert l1.daemon.is_in_log("Missing a Bitcoin plugin command")
|
assert l1.daemon.wait() == 1
|
||||||
|
assert l1.daemon.is_in_stderr(r"Could not access the plugin for sendrawtransaction")
|
||||||
# Now we should start if all the commands are registered, even if they
|
# Now we should start if all the commands are registered, even if they
|
||||||
# are registered by two distincts plugins.
|
# are registered by two distincts plugins.
|
||||||
del l1.daemon.opts["plugin"]
|
del l1.daemon.opts["plugin"]
|
||||||
l1.daemon.opts["plugin-dir"] = os.path.join(os.getcwd(),
|
l1.daemon.opts["plugin-dir"] = os.path.join(os.getcwd(),
|
||||||
"tests/plugins/bitcoin/")
|
"tests/plugins/bitcoin/")
|
||||||
try:
|
# (it fails when it tries to use them, so startup fails)
|
||||||
l1.daemon.start()
|
l1.daemon.start(wait_for_initialized=False)
|
||||||
except ValueError:
|
l1.daemon.wait_for_log("All Bitcoin plugin commands registered")
|
||||||
msg = "All Bitcoin plugin commands registered"
|
assert l1.daemon.wait() == 1
|
||||||
assert l1.daemon.is_in_log(msg)
|
|
||||||
else:
|
|
||||||
raise Exception("We registered all commands but couldn't start!")
|
|
||||||
else:
|
|
||||||
raise Exception("We could start without all commands registered !!")
|
|
||||||
|
|
||||||
# But restarting with just bcli is ok
|
# But restarting with just bcli is ok
|
||||||
del l1.daemon.opts["plugin-dir"]
|
del l1.daemon.opts["plugin-dir"]
|
||||||
@@ -2074,73 +2071,55 @@ def test_important_plugin(node_factory):
|
|||||||
n = node_factory.get_node(options={"important-plugin": os.path.join(pluginsdir, "nonexistent")},
|
n = node_factory.get_node(options={"important-plugin": os.path.join(pluginsdir, "nonexistent")},
|
||||||
may_fail=True, expect_fail=True,
|
may_fail=True, expect_fail=True,
|
||||||
allow_broken_log=True, start=False)
|
allow_broken_log=True, start=False)
|
||||||
n.daemon.start(wait_for_initialized=False, stderr=subprocess.PIPE)
|
n.daemon.start(wait_for_initialized=False)
|
||||||
wait_for(lambda: not n.daemon.running)
|
# Will exit with failure code.
|
||||||
|
assert n.daemon.wait() == 1
|
||||||
assert n.daemon.is_in_stderr(r"error starting plugin '.*nonexistent'")
|
assert n.daemon.is_in_stderr(r"error starting plugin '.*nonexistent'")
|
||||||
|
|
||||||
# We use a log file, since our wait_for_log is unreliable when the
|
|
||||||
# daemon actually dies.
|
|
||||||
def get_logfile_match(logpath, regex):
|
|
||||||
if not os.path.exists(logpath):
|
|
||||||
return None
|
|
||||||
with open(logpath, 'r') as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
m = re.search(regex, line)
|
|
||||||
if m is not None:
|
|
||||||
return m
|
|
||||||
return None
|
|
||||||
|
|
||||||
logpath = os.path.join(n.daemon.lightning_dir, TEST_NETWORK, 'logfile')
|
|
||||||
n.daemon.opts['log-file'] = 'logfile'
|
|
||||||
|
|
||||||
# Check we exit if the important plugin dies.
|
# Check we exit if the important plugin dies.
|
||||||
n.daemon.opts['important-plugin'] = os.path.join(pluginsdir, "fail_by_itself.py")
|
n.daemon.opts['important-plugin'] = os.path.join(pluginsdir, "fail_by_itself.py")
|
||||||
|
|
||||||
n.daemon.start(wait_for_initialized=False)
|
n.daemon.start(wait_for_initialized=False)
|
||||||
wait_for(lambda: not n.daemon.running)
|
# Will exit with failure code.
|
||||||
|
assert n.daemon.wait() == 1
|
||||||
assert get_logfile_match(logpath,
|
n.daemon.wait_for_log(r'fail_by_itself.py: Plugin marked as important, shutting down lightningd')
|
||||||
r'fail_by_itself.py: Plugin marked as important, shutting down lightningd')
|
|
||||||
os.remove(logpath)
|
|
||||||
|
|
||||||
# Check if the important plugin is disabled, we run as normal.
|
# Check if the important plugin is disabled, we run as normal.
|
||||||
n.daemon.opts['disable-plugin'] = "fail_by_itself.py"
|
n.daemon.opts['disable-plugin'] = "fail_by_itself.py"
|
||||||
del n.daemon.opts['log-file']
|
|
||||||
n.daemon.start()
|
n.daemon.start()
|
||||||
# Make sure we can call into a plugin RPC (this is from `bcli`) even
|
# Make sure we can call into a plugin RPC (this is from `bcli`) even
|
||||||
# if fail_by_itself.py is disabled.
|
# if fail_by_itself.py is disabled.
|
||||||
n.rpc.call("estimatefees", {})
|
n.rpc.call("estimatefees", {})
|
||||||
# Make sure we are still running.
|
|
||||||
assert n.daemon.running
|
|
||||||
n.stop()
|
n.stop()
|
||||||
|
|
||||||
# Check if an important plugin dies later, we fail.
|
# Check if an important plugin dies later, we fail.
|
||||||
del n.daemon.opts['disable-plugin']
|
del n.daemon.opts['disable-plugin']
|
||||||
n.daemon.opts['log-file'] = 'logfile'
|
|
||||||
n.daemon.opts['important-plugin'] = os.path.join(pluginsdir, "suicidal_plugin.py")
|
n.daemon.opts['important-plugin'] = os.path.join(pluginsdir, "suicidal_plugin.py")
|
||||||
|
|
||||||
n.daemon.start(wait_for_initialized=False)
|
n.start()
|
||||||
wait_for(lambda: get_logfile_match(logpath, "Server started with public key"))
|
|
||||||
|
|
||||||
with pytest.raises(RpcError):
|
with pytest.raises(RpcError):
|
||||||
n.rpc.call("die", {})
|
n.rpc.call("die", {})
|
||||||
|
|
||||||
wait_for(lambda: not n.daemon.running)
|
# Should exit with exitcode 1
|
||||||
assert get_logfile_match(logpath, 'suicidal_plugin.py: Plugin marked as important, shutting down lightningd')
|
n.daemon.wait_for_log('suicidal_plugin.py: Plugin marked as important, shutting down lightningd')
|
||||||
os.remove(logpath)
|
assert n.daemon.wait() == 1
|
||||||
|
n.stop()
|
||||||
|
|
||||||
# Check that if a builtin plugin dies, we fail.
|
# Check that if a builtin plugin dies, we fail.
|
||||||
n.daemon.start(wait_for_initialized=False)
|
start = n.daemon.logsearch_start
|
||||||
|
n.start()
|
||||||
wait_for(lambda: get_logfile_match(logpath, r'.*started\(([0-9]*)\).*plugins/pay'))
|
# Reset logsearch_start, since this will predate message that start() looks for.
|
||||||
pidstr = get_logfile_match(logpath, r'.*started\(([0-9]*)\).*plugins/pay').group(1)
|
n.daemon.logsearch_start = start
|
||||||
|
line = n.daemon.wait_for_log(r'.*started\([0-9]*\).*plugins/pay')
|
||||||
|
pidstr = re.search(r'.*started\(([0-9]*)\).*plugins/pay', line).group(1)
|
||||||
|
|
||||||
# Kill pay.
|
# Kill pay.
|
||||||
os.kill(int(pidstr), signal.SIGKILL)
|
os.kill(int(pidstr), signal.SIGKILL)
|
||||||
wait_for(lambda: not n.daemon.running)
|
n.daemon.wait_for_log('pay: Plugin marked as important, shutting down lightningd')
|
||||||
|
# Should exit with exitcode 1
|
||||||
assert get_logfile_match(logpath, 'pay: Plugin marked as important, shutting down lightningd')
|
assert n.daemon.wait() == 1
|
||||||
|
n.stop()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.developer("tests developer-only option.")
|
@pytest.mark.developer("tests developer-only option.")
|
||||||
|
|||||||
@@ -1038,12 +1038,11 @@ def test_hsm_secret_encryption(node_factory):
|
|||||||
|
|
||||||
# Test we cannot restore the same wallet with another password
|
# Test we cannot restore the same wallet with another password
|
||||||
l1.daemon.opts.update({"encrypted-hsm": None})
|
l1.daemon.opts.update({"encrypted-hsm": None})
|
||||||
l1.daemon.start(stdin=slave_fd, stderr=subprocess.STDOUT,
|
l1.daemon.start(stdin=slave_fd, wait_for_initialized=False)
|
||||||
wait_for_initialized=False)
|
|
||||||
l1.daemon.wait_for_log(r'Enter hsm_secret password')
|
l1.daemon.wait_for_log(r'Enter hsm_secret password')
|
||||||
write_all(master_fd, password[2:].encode("utf-8"))
|
write_all(master_fd, password[2:].encode("utf-8"))
|
||||||
assert(l1.daemon.proc.wait(WAIT_TIMEOUT) == HSM_BAD_PASSWORD)
|
assert(l1.daemon.proc.wait(WAIT_TIMEOUT) == HSM_BAD_PASSWORD)
|
||||||
assert(l1.daemon.is_in_log("Wrong password for encrypted hsm_secret."))
|
assert(l1.daemon.is_in_stderr("Wrong password for encrypted hsm_secret."))
|
||||||
|
|
||||||
# Not sure why this helps, but seems to reduce flakiness where
|
# Not sure why this helps, but seems to reduce flakiness where
|
||||||
# tail() thread in testing/utils.py gets 'ValueError: readline of
|
# tail() thread in testing/utils.py gets 'ValueError: readline of
|
||||||
@@ -1069,9 +1068,9 @@ def test_hsm_secret_encryption(node_factory):
|
|||||||
|
|
||||||
class HsmTool(TailableProc):
|
class HsmTool(TailableProc):
|
||||||
"""Helper for testing the hsmtool as a subprocess"""
|
"""Helper for testing the hsmtool as a subprocess"""
|
||||||
def __init__(self, *args):
|
def __init__(self, directory, *args):
|
||||||
self.prefix = "hsmtool"
|
self.prefix = "hsmtool"
|
||||||
TailableProc.__init__(self)
|
TailableProc.__init__(self, os.path.join(directory, "hsmtool"))
|
||||||
assert hasattr(self, "env")
|
assert hasattr(self, "env")
|
||||||
self.cmd_line = ["tools/hsmtool", *args]
|
self.cmd_line = ["tools/hsmtool", *args]
|
||||||
|
|
||||||
@@ -1098,9 +1097,8 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
# We can't use a wrong password !
|
# We can't use a wrong password !
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool = HsmTool("decrypt", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path)
|
||||||
hsmtool.start(stdin=slave_fd,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
||||||
write_all(master_fd, "A wrong pass\n\n".encode("utf-8"))
|
write_all(master_fd, "A wrong pass\n\n".encode("utf-8"))
|
||||||
hsmtool.proc.wait(WAIT_TIMEOUT)
|
hsmtool.proc.wait(WAIT_TIMEOUT)
|
||||||
@@ -1108,8 +1106,7 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
# Decrypt it with hsmtool
|
# Decrypt it with hsmtool
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool.start(stdin=slave_fd,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
||||||
write_all(master_fd, password.encode("utf-8"))
|
write_all(master_fd, password.encode("utf-8"))
|
||||||
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
||||||
@@ -1122,9 +1119,8 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
# Test we can encrypt it offline
|
# Test we can encrypt it offline
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool = HsmTool("encrypt", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "encrypt", hsm_path)
|
||||||
hsmtool.start(stdin=slave_fd,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
||||||
write_all(master_fd, password.encode("utf-8"))
|
write_all(master_fd, password.encode("utf-8"))
|
||||||
hsmtool.wait_for_log(r"Confirm hsm_secret password:")
|
hsmtool.wait_for_log(r"Confirm hsm_secret password:")
|
||||||
@@ -1137,7 +1133,7 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
l1.daemon.opts.update({"encrypted-hsm": None})
|
l1.daemon.opts.update({"encrypted-hsm": None})
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
l1.daemon.start(stdin=slave_fd, stderr=subprocess.STDOUT,
|
l1.daemon.start(stdin=slave_fd,
|
||||||
wait_for_initialized=False)
|
wait_for_initialized=False)
|
||||||
|
|
||||||
l1.daemon.wait_for_log(r'The hsm_secret is encrypted')
|
l1.daemon.wait_for_log(r'The hsm_secret is encrypted')
|
||||||
@@ -1149,9 +1145,8 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
# And finally test that we can also decrypt if encrypted with hsmtool
|
# And finally test that we can also decrypt if encrypted with hsmtool
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool = HsmTool("decrypt", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path)
|
||||||
hsmtool.start(stdin=slave_fd,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
hsmtool.wait_for_log(r"Enter hsm_secret password:")
|
||||||
write_all(master_fd, password.encode("utf-8"))
|
write_all(master_fd, password.encode("utf-8"))
|
||||||
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
||||||
@@ -1161,9 +1156,8 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
|
|
||||||
# We can roundtrip encryption and decryption using a password provided
|
# We can roundtrip encryption and decryption using a password provided
|
||||||
# through stdin.
|
# through stdin.
|
||||||
hsmtool = HsmTool("encrypt", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "encrypt", hsm_path)
|
||||||
hsmtool.start(stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
hsmtool.start(stdin=subprocess.PIPE)
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
hsmtool.proc.stdin.write(password.encode("utf-8"))
|
hsmtool.proc.stdin.write(password.encode("utf-8"))
|
||||||
hsmtool.proc.stdin.write(password.encode("utf-8"))
|
hsmtool.proc.stdin.write(password.encode("utf-8"))
|
||||||
hsmtool.proc.stdin.flush()
|
hsmtool.proc.stdin.flush()
|
||||||
@@ -1171,9 +1165,8 @@ def test_hsmtool_secret_decryption(node_factory):
|
|||||||
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
|
||||||
|
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool = HsmTool("decrypt", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path)
|
||||||
hsmtool.start(stdin=slave_fd,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log("Enter hsm_secret password:")
|
hsmtool.wait_for_log("Enter hsm_secret password:")
|
||||||
write_all(master_fd, password.encode("utf-8"))
|
write_all(master_fd, password.encode("utf-8"))
|
||||||
hsmtool.wait_for_log("Successfully decrypted")
|
hsmtool.wait_for_log("Successfully decrypted")
|
||||||
@@ -1232,19 +1225,17 @@ def test_hsmtool_generatehsm(node_factory):
|
|||||||
hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK,
|
hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK,
|
||||||
"hsm_secret")
|
"hsm_secret")
|
||||||
|
|
||||||
hsmtool = HsmTool("generatehsm", hsm_path)
|
hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path)
|
||||||
|
|
||||||
# You cannot re-generate an already existing hsm_secret
|
# You cannot re-generate an already existing hsm_secret
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool.start(stdin=slave_fd, stdout=subprocess.PIPE,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 2
|
assert hsmtool.proc.wait(WAIT_TIMEOUT) == 2
|
||||||
os.remove(hsm_path)
|
os.remove(hsm_path)
|
||||||
|
|
||||||
# We can generate a valid hsm_secret from a wordlist and a "passphrase"
|
# We can generate a valid hsm_secret from a wordlist and a "passphrase"
|
||||||
master_fd, slave_fd = os.openpty()
|
master_fd, slave_fd = os.openpty()
|
||||||
hsmtool.start(stdin=slave_fd, stdout=subprocess.PIPE,
|
hsmtool.start(stdin=slave_fd)
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
hsmtool.wait_for_log(r"Select your language:")
|
hsmtool.wait_for_log(r"Select your language:")
|
||||||
write_all(master_fd, "0\n".encode("utf-8"))
|
write_all(master_fd, "0\n".encode("utf-8"))
|
||||||
hsmtool.wait_for_log(r"Introduce your BIP39 word list")
|
hsmtool.wait_for_log(r"Introduce your BIP39 word list")
|
||||||
|
|||||||
Reference in New Issue
Block a user