mirror of
https://github.com/ElementsProject/lightning.git
synced 2026-08-13 12:32:55 +02:00
Some checks are pending
Continuous Integration / Pre-build checks (push) Waiting to run
Continuous Integration / Build compile-clang-sanitizers (push) Blocked by required conditions
Continuous Integration / Build compile-clang (push) Blocked by required conditions
Continuous Integration / Build compile-gcc (push) Blocked by required conditions
Continuous Integration / Build compile-gcc-O1 (push) Blocked by required conditions
Continuous Integration / Build compile-gcc-O3 (push) Blocked by required conditions
Continuous Integration / check-compiled-source (compile-gcc) (push) Blocked by required conditions
Continuous Integration / Run unit tests (push) Blocked by required conditions
Continuous Integration / Run unit tests-1 (push) Blocked by required conditions
Continuous Integration / Build 32-bit (size_t != 64-bit warnings) (push) Blocked by required conditions
Continuous Integration / Run fuzz regression tests (push) Blocked by required conditions
Continuous Integration / Check we can downgrade the node (push) Blocked by required conditions
Continuous Integration / Check we can downgrade the node-1 (push) Blocked by required conditions
Continuous Integration / Check we can downgrade the node-2 (push) Blocked by required conditions
Continuous Integration / First Integration Tests (1/6) (push) Blocked by required conditions
Continuous Integration / First Integration Tests (2/6) (push) Blocked by required conditions
Continuous Integration / First Integration Tests (3/6) (push) Blocked by required conditions
Continuous Integration / First Integration Tests (4/6) (push) Blocked by required conditions
Continuous Integration / First Integration Tests (5/6) (push) Blocked by required conditions
Continuous Integration / First Integration Tests (6/6) (push) Blocked by required conditions
Continuous Integration / Test CLN dual-fund Full Integration (push) Blocked by required conditions
Continuous Integration / Test CLN liquid Full Integration (push) Blocked by required conditions
Continuous Integration / Test CLN postgres Full Integration (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (1/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (10/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (11/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (12/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (2/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (3/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (4/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (5/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (6/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (7/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (8/12) (push) Blocked by required conditions
Continuous Integration / Valgrind Test CLN (9/12) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (1/6) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (2/6) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (3/6) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (4/6) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (5/6) (push) Blocked by required conditions
Continuous Integration / ASan/UBSan (6/6) (push) Blocked by required conditions
Continuous Integration / Update examples in doc schemas (push) Blocked by required conditions
Continuous Integration / Test minimum supported BTC v25.0 with clang (push) Blocked by required conditions
Continuous Integration / CI completion (push) Blocked by required conditions
Release Rust 🦀 / release_rust (push) Waiting to run
The canned_github_server fixture Popens a Flask server and never waits for it to start listening; the git repository setup between the Popen and the yield usually gives it enough time. Under CI load it can lose that race: a valgrind-shard failure shows test_search's first reckless invocation getting connection refused within a second of the test starting, while Flask's "Running on http://127.0.0.1:..." banner only appears in the log after the test had already failed. Wait for the port to accept connections before yielding, and fail loudly if the server process dies instead of coming up. Changelog-None
411 lines
17 KiB
Python
411 lines
17 KiB
Python
from fixtures import * # noqa: F401,F403
|
|
import subprocess
|
|
from pathlib import PosixPath, Path
|
|
import socket
|
|
from pyln.testing.utils import TIMEOUT, VALGRIND
|
|
import pytest
|
|
import os
|
|
import re
|
|
import shutil
|
|
import time
|
|
import unittest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def canned_github_server(directory):
|
|
global NETWORK
|
|
NETWORK = os.environ.get('TEST_NETWORK')
|
|
if NETWORK is None:
|
|
NETWORK = 'regtest'
|
|
FILE_PATH = Path(os.path.dirname(os.path.realpath(__file__)))
|
|
if os.environ.get('LIGHTNING_CLI') is None:
|
|
os.environ['LIGHTNING_CLI'] = str(FILE_PATH.parent / 'cli/lightning-cli')
|
|
print('LIGHTNING_CALL: ', os.environ.get('LIGHTNING_CLI'))
|
|
# Use socket to provision a random free port
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.bind(('localhost', 0))
|
|
free_port = str(sock.getsockname()[1])
|
|
sock.close()
|
|
global my_env
|
|
my_env = os.environ.copy()
|
|
# This tells reckless to redirect to the canned server rather than github.
|
|
my_env['REDIR_GITHUB_API'] = f'http://127.0.0.1:{free_port}/api'
|
|
my_env['REDIR_GITHUB'] = directory
|
|
my_env['FLASK_RUN_PORT'] = free_port
|
|
my_env['FLASK_APP'] = str(FILE_PATH / 'rkls_github_canned_server')
|
|
server = subprocess.Popen(["python3", "-m", "flask", "run"],
|
|
env=my_env)
|
|
|
|
# Generate test plugin repository to test reckless against.
|
|
repo_dir = os.path.join(directory, "lightningd")
|
|
os.mkdir(repo_dir, 0o777)
|
|
plugins_path = str(FILE_PATH / 'data/recklessrepo/lightningd')
|
|
|
|
# Create requirements.txt file for the testpluginpass
|
|
# with pyln-client installed from the local source
|
|
requirements_file_path = os.path.join(plugins_path, 'testplugpass', 'requirements.txt')
|
|
with open(requirements_file_path, 'w') as f:
|
|
pyln_client_path = os.path.abspath(os.path.join(FILE_PATH, '..', 'contrib', 'pyln-client'))
|
|
f.write(f"pyln-client @ file://{pyln_client_path}\n")
|
|
|
|
# This lets us temporarily set .gitconfig user info in order to commit
|
|
my_env['HOME'] = directory
|
|
with open(os.path.join(directory, '.gitconfig'), 'w') as conf:
|
|
conf.write(("[user]\n"
|
|
"\temail = reckless@example.com\n"
|
|
"\tname = reckless CI\n"
|
|
"\t[init]\n"
|
|
"\tdefaultBranch = master"))
|
|
|
|
with open(os.path.join(directory, '.gitconfig'), 'r') as conf:
|
|
print(conf.readlines())
|
|
|
|
# Bare repository must be initialized prior to setting other git env vars
|
|
subprocess.check_output(['git', 'init', '--bare', 'plugins'], cwd=repo_dir,
|
|
env=my_env)
|
|
|
|
my_env['GIT_DIR'] = os.path.join(repo_dir, 'plugins')
|
|
my_env['GIT_WORK_TREE'] = repo_dir
|
|
my_env['GIT_INDEX_FILE'] = os.path.join(repo_dir, 'scratch-index')
|
|
repo_initialization = (f'cp -r {plugins_path}/* .;'
|
|
'git add --all;'
|
|
'git commit -m "initial commit - autogenerated by test_reckless.py";')
|
|
tag_and_update = ('git tag v1;'
|
|
"sed -i 's/v1/v2/g' testplugpass/testplugpass.py;"
|
|
'git add testplugpass/testplugpass.py;'
|
|
'git commit -m "update to v2";'
|
|
'git tag v2;')
|
|
subprocess.check_output([repo_initialization], env=my_env, shell=True,
|
|
cwd=repo_dir)
|
|
subprocess.check_output([tag_and_update], env=my_env,
|
|
shell=True, cwd=repo_dir)
|
|
del my_env['HOME']
|
|
del my_env['GIT_DIR']
|
|
del my_env['GIT_WORK_TREE']
|
|
del my_env['GIT_INDEX_FILE']
|
|
# We also need the github api data for the repo which will be served via http
|
|
shutil.copyfile(str(FILE_PATH / 'data/recklessrepo/rkls_api_lightningd_plugins.json'), os.path.join(directory, 'rkls_api_lightningd_plugins.json'))
|
|
|
|
# Flask can be slow to start listening under CI load; make sure the
|
|
# first reckless invocation doesn't race it and get connection refused.
|
|
start = time.time()
|
|
while True:
|
|
assert server.poll() is None, "canned github server died"
|
|
try:
|
|
with socket.create_connection(('127.0.0.1', int(free_port)),
|
|
timeout=5):
|
|
break
|
|
except OSError:
|
|
if time.time() >= start + TIMEOUT:
|
|
server.terminate()
|
|
raise RuntimeError("canned github server never started listening")
|
|
time.sleep(0.2)
|
|
yield
|
|
# Delete requirements.txt from the testplugpass directory
|
|
with open(requirements_file_path, 'w') as f:
|
|
f.write(f"pyln-client\n\n")
|
|
server.terminate()
|
|
|
|
|
|
class RecklessResult:
|
|
def __init__(self, process, returncode, stdout, stderr):
|
|
self.process = process
|
|
self.returncode = returncode
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
|
|
def __repr__(self):
|
|
return f'self.returncode, self.stdout, self.stderr'
|
|
|
|
def search_stdout(self, regex):
|
|
"""return the matching regex line from reckless output."""
|
|
ex = re.compile(regex)
|
|
matching = []
|
|
for line in self.stdout:
|
|
if ex.search(line):
|
|
matching.append(line)
|
|
return matching
|
|
|
|
def check_stderr(self):
|
|
def output_okay(out):
|
|
for warning in ['[notice]', 'WARNING:', 'npm WARN',
|
|
'npm notice', 'DEPRECATION:', 'Creating virtualenv',
|
|
'config file not found:', 'press [Y]']:
|
|
if out.startswith(warning):
|
|
return True
|
|
return False
|
|
for e in self.stderr:
|
|
if len(e) < 1:
|
|
continue
|
|
# Don't err on verbosity from pip, npm
|
|
if not output_okay(e):
|
|
raise Exception(f'reckless stderr contains `{e}`')
|
|
|
|
|
|
def reckless(cmds: list, dir: PosixPath = None,
|
|
autoconfirm=True, timeout: int = 60):
|
|
'''Call the reckless executable, optionally with a directory.'''
|
|
if dir is not None:
|
|
cmds.insert(0, "-l")
|
|
cmds.insert(1, str(dir))
|
|
cmds.insert(0, "tools/reckless")
|
|
if autoconfirm:
|
|
process_input = 'Y\n'
|
|
else:
|
|
process_input = None
|
|
r = subprocess.run(cmds, capture_output=True, encoding='utf-8', env=my_env,
|
|
input=process_input, timeout=timeout)
|
|
stdout = r.stdout.splitlines()
|
|
stderr = r.stderr.splitlines()
|
|
print(" ".join(r.args), "\n")
|
|
print("***RECKLESS STDOUT***")
|
|
for l in stdout:
|
|
print(l)
|
|
print('\n')
|
|
print("***RECKLESS STDERR***")
|
|
for l in stderr:
|
|
print(l)
|
|
print('\n')
|
|
return RecklessResult(r, r.returncode, stdout, stderr)
|
|
|
|
|
|
def get_reckless_node(node_factory):
|
|
'''This may be unnecessary, but a preconfigured lightning dir
|
|
is useful for reckless testing.'''
|
|
node = node_factory.get_node(options={}, start=False)
|
|
return node
|
|
|
|
|
|
def test_basic_help():
|
|
'''Validate that argparse provides basic help info.
|
|
This requires no config options passed to reckless.'''
|
|
r = reckless(["-h"])
|
|
assert r.returncode == 0
|
|
assert r.search_stdout("positional arguments:")
|
|
assert r.search_stdout("options:") or r.search_stdout("optional arguments:")
|
|
|
|
|
|
def test_contextual_help(node_factory):
|
|
n = get_reckless_node(node_factory)
|
|
for subcmd in ['install', 'uninstall', 'search',
|
|
'enable', 'disable', 'source']:
|
|
r = reckless([subcmd, "-h"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout("positional arguments:")
|
|
|
|
|
|
def test_sources(node_factory):
|
|
"""add additional sources and search through them"""
|
|
n = get_reckless_node(node_factory)
|
|
r = reckless(["source", "-h"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
r = reckless(["source", "list"], dir=n.lightning_dir)
|
|
print(r.stdout)
|
|
assert r.returncode == 0
|
|
print(n.lightning_dir)
|
|
reckless_dir = Path(n.lightning_dir) / 'reckless'
|
|
print(dir(reckless_dir))
|
|
assert (reckless_dir / '.sources').exists()
|
|
print(os.listdir(reckless_dir))
|
|
print(reckless_dir / '.sources')
|
|
plugin_dir = str(os.path.join(n.lightning_dir, '..', 'lightningd'))
|
|
r = reckless([f"--network={NETWORK}", "-v", "source", "add",
|
|
f'{plugin_dir}/testplugfail'],
|
|
dir=n.lightning_dir)
|
|
r = reckless([f"--network={NETWORK}", "-v", "source", "add",
|
|
f'{plugin_dir}/testplugpass'],
|
|
dir=n.lightning_dir)
|
|
with open(reckless_dir / '.sources') as sources:
|
|
contents = [c.strip() for c in sources.readlines()]
|
|
print('contents:', contents)
|
|
assert 'https://github.com/lightningd/plugins' in contents
|
|
assert f'{plugin_dir}/testplugfail' in contents
|
|
assert f'{plugin_dir}/testplugpass' in contents
|
|
r = reckless([f"--network={NETWORK}", "-v", "source", "remove",
|
|
f'{plugin_dir}/testplugfail'],
|
|
dir=n.lightning_dir)
|
|
with open(reckless_dir / '.sources') as sources:
|
|
contents = [c.strip() for c in sources.readlines()]
|
|
print('contents:', contents)
|
|
assert f'{plugin_dir}/testplugfail' not in contents
|
|
assert f'{plugin_dir}/testplugpass' in contents
|
|
|
|
|
|
def test_search(node_factory):
|
|
"""add additional sources and search through them"""
|
|
n = get_reckless_node(node_factory)
|
|
r = reckless([f"--network={NETWORK}", "search", "testplugpass"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('found testplugpass in source: https://github.com/lightningd/plugins')
|
|
|
|
|
|
def test_search_partial_match(node_factory):
|
|
"""test that partial/substring search returns multiple matches"""
|
|
n = get_reckless_node(node_factory)
|
|
|
|
# Search for partial name "testplug" - should find all test plugins
|
|
r = reckless([f"--network={NETWORK}", "search", "testplug"], dir=n.lightning_dir)
|
|
# Should show the "Plugins matching" header
|
|
assert r.search_stdout("Plugins matching 'testplug':")
|
|
# Should list multiple plugins (all start with "testplug")
|
|
assert r.search_stdout('testplugpass')
|
|
assert r.search_stdout('testplugfail')
|
|
assert r.search_stdout('testplugpyproj')
|
|
assert r.search_stdout('testpluguv')
|
|
|
|
# Search for "pass" - should find testplugpass
|
|
r = reckless([f"--network={NETWORK}", "search", "pass"], dir=n.lightning_dir)
|
|
assert r.search_stdout("Plugins matching 'pass':")
|
|
assert r.search_stdout('testplugpass')
|
|
# Should not find plugins without "pass" in name
|
|
assert not r.search_stdout('testplugfail')
|
|
|
|
# Search for something that doesn't exist
|
|
r = reckless([f"--network={NETWORK}", "search", "nonexistent"], dir=n.lightning_dir)
|
|
assert r.search_stdout("Search exhausted all sources")
|
|
|
|
|
|
def test_install(node_factory):
|
|
"""test search, git clone, and installation to folder."""
|
|
n = get_reckless_node(node_factory)
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpass"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('dependencies installed successfully')
|
|
assert r.search_stdout('plugin installed:')
|
|
assert r.search_stdout('testplugpass enabled')
|
|
r.check_stderr()
|
|
plugin_path = Path(n.lightning_dir) / 'reckless/testplugpass'
|
|
print(plugin_path)
|
|
assert os.path.exists(plugin_path)
|
|
|
|
|
|
@unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection")
|
|
def test_poetry_install(node_factory):
|
|
"""test search, git clone, and installation to folder."""
|
|
n = get_reckless_node(node_factory)
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpyproj"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('dependencies installed successfully')
|
|
assert r.search_stdout('plugin installed:')
|
|
assert r.search_stdout('testplugpyproj enabled')
|
|
r.check_stderr()
|
|
plugin_path = Path(n.lightning_dir) / 'reckless/testplugpyproj'
|
|
print(plugin_path)
|
|
assert os.path.exists(plugin_path)
|
|
n.start()
|
|
print(n.rpc.testmethod())
|
|
assert n.daemon.is_in_log(r'plugin-manager: started\([0-9].*\) /tmp/ltests-[a-z0-9_].*/test_poetry_install_1/lightning-1/reckless/testplugpyproj/testplugpyproj.py')
|
|
assert n.rpc.testmethod() == 'I live.'
|
|
|
|
|
|
@unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection")
|
|
def test_local_dir_install(node_factory):
|
|
"""Test search and install from local directory source."""
|
|
n = get_reckless_node(node_factory)
|
|
n.start()
|
|
source_dir = str(Path(n.lightning_dir / '..' / 'lightningd' / 'testplugpass').resolve())
|
|
r = reckless([f"--network={NETWORK}", "-v", "source", "add", source_dir], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpass"], dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('testplugpass enabled')
|
|
plugin_path = Path(n.lightning_dir) / 'reckless/testplugpass'
|
|
print(plugin_path)
|
|
assert os.path.exists(plugin_path)
|
|
|
|
# Retry with a direct install passing the full path to the local source
|
|
r = reckless(['uninstall', 'testplugpass', '-v'], dir=n.lightning_dir)
|
|
assert not os.path.exists(plugin_path)
|
|
r = reckless(['source', 'remove', source_dir], dir=n.lightning_dir)
|
|
assert r.search_stdout('plugin source removed')
|
|
r = reckless(['install', '-v', source_dir], dir=n.lightning_dir)
|
|
assert r.search_stdout('testplugpass enabled')
|
|
assert os.path.exists(plugin_path)
|
|
|
|
|
|
@unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection")
|
|
def test_disable_enable(node_factory):
|
|
"""test search, git clone, and installation to folder."""
|
|
n = get_reckless_node(node_factory)
|
|
# Test case-insensitive search as well
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testPlugPass"],
|
|
dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('dependencies installed successfully')
|
|
assert r.search_stdout('plugin installed:')
|
|
assert r.search_stdout('testplugpass enabled')
|
|
r.check_stderr()
|
|
plugin_path = Path(n.lightning_dir) / 'reckless/testplugpass'
|
|
print(plugin_path)
|
|
assert os.path.exists(plugin_path)
|
|
r = reckless([f"--network={NETWORK}", "-v", "disable", "testPlugPass"],
|
|
dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
n.start()
|
|
# Should find it with or without the file extension
|
|
r = reckless([f"--network={NETWORK}", "-v", "enable", "testplugpass.py"],
|
|
dir=n.lightning_dir)
|
|
assert r.returncode == 0
|
|
assert r.search_stdout('testplugpass enabled')
|
|
test_plugin = {'name': str(plugin_path / 'testplugpass.py'),
|
|
'active': True, 'dynamic': True}
|
|
time.sleep(1)
|
|
print(n.rpc.plugin_list()['plugins'])
|
|
assert test_plugin in n.rpc.plugin_list()['plugins']
|
|
|
|
|
|
@unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection")
|
|
def test_tag_install(node_factory):
|
|
"install a plugin from a specific commit hash or tag"
|
|
node = get_reckless_node(node_factory)
|
|
node.start()
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testPlugPass"],
|
|
dir=node.lightning_dir)
|
|
assert r.returncode == 0
|
|
metadata = node.lightning_dir / "reckless/testplugpass/.metadata"
|
|
with open(metadata, "r") as md:
|
|
header = ''
|
|
for line in md.readlines():
|
|
line = line.strip()
|
|
if header == 'requested commit':
|
|
assert line == 'None'
|
|
header = line
|
|
# should install v2 (latest) without specifying
|
|
version = node.rpc.gettestplugversion()
|
|
assert version == 'v2'
|
|
r = reckless([f"--network={NETWORK}", "-v", "uninstall", "testplugpass"],
|
|
dir=node.lightning_dir)
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpass@v1"],
|
|
dir=node.lightning_dir)
|
|
assert r.returncode == 0
|
|
# v1 should now be checked out.
|
|
version = node.rpc.gettestplugversion()
|
|
assert version == 'v1'
|
|
installed_path = Path(node.lightning_dir) / 'reckless/testplugpass'
|
|
assert installed_path.is_dir()
|
|
with open(metadata, "r") as md:
|
|
header = ''
|
|
for line in md.readlines():
|
|
line = line.strip()
|
|
if header == 'requested commit':
|
|
assert line == 'v1'
|
|
header = line
|
|
|
|
|
|
# Note: uv timeouts from the GH network seem to happen?
|
|
@pytest.mark.slow_test
|
|
@pytest.mark.flaky(reruns=3)
|
|
def test_reckless_uv_install(node_factory):
|
|
node = get_reckless_node(node_factory)
|
|
node.start()
|
|
r = reckless([f"--network={NETWORK}", "-v", "install", "testpluguv"],
|
|
dir=node.lightning_dir)
|
|
assert r.returncode == 0
|
|
installed_path = Path(node.lightning_dir) / 'reckless/testpluguv'
|
|
assert installed_path.is_dir()
|
|
assert node.rpc.uvplugintest() == 'I live.'
|
|
version = node.rpc.getuvpluginversion()
|
|
assert version == 'v1'
|
|
|
|
assert r.search_stdout('using installer pythonuv')
|
|
r.check_stderr()
|