-
Notifications
You must be signed in to change notification settings - Fork 990
Expand file tree
/
Copy pathtest_reckless.py
More file actions
394 lines (350 loc) · 16.1 KB
/
test_reckless.py
File metadata and controls
394 lines (350 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
from fixtures import * # noqa: F401,F403
import subprocess
from pathlib import PosixPath, Path
import socket
from pyln.testing.utils import VALGRIND, SLOW_MACHINE
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'))
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
@unittest.skipIf(VALGRIND and SLOW_MACHINE, "node too slow for starting plugin under valgrind")
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()