Skip to content

Reckless uv installer #8430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
9 changes: 9 additions & 0 deletions tests/data/recklessrepo/lightningd/testpluguv/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "testpluguv"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pyln-client>=24.4",
]
25 changes: 25 additions & 0 deletions tests/data/recklessrepo/lightningd/testpluguv/testpluguv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
from pyln.client import Plugin

plugin = Plugin()

__version__ = 'v1'


@plugin.init()
def init(options, configuration, plugin, **kwargs):
plugin.log("testpluguv initialized")


@plugin.method("uvplugintest")
def uvplugintest(plugin):
return "I live."


@plugin.method("getuvpluginversion")
def getuvpluginversion(plugin):
"to test commit/tag checkout"
return __version__


plugin.run()
188 changes: 188 additions & 0 deletions tests/data/recklessrepo/lightningd/testpluguv/uv.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions tests/data/recklessrepo/rkls_api_lightningd_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
"download_url": null,
"type": "dir"
},
{
"name": "testpluguv",
"path": "testpluguv",
"url": "https://api.github.com/repos/lightningd/plugins/contents/webhook?ref=master",
"html_url": "https://github.com/lightningd/plugins/tree/master/testpluguv",
"git_url": "https://api.github.com/repos/lightningd/plugins/git/trees/testpluguv",
"download_url": null,
"type": "dir"
},
{
"name": "testplugfail",
"path": "testplugfail",
Expand Down
123 changes: 83 additions & 40 deletions tests/test_reckless.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pyln.testing.utils import VALGRIND
import pytest
import os
import re
import shutil
import time
import unittest
Expand Down Expand Up @@ -91,25 +92,66 @@ def canned_github_server(directory):
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 = 15):
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='Y\n')
input=process_input, timeout=timeout)
stdout = r.stdout.splitlines()
stderr = r.stderr.splitlines()
print(" ".join(r.args), "\n")
print("***RECKLESS STDOUT***")
for l in r.stdout.splitlines():
for l in stdout:
print(l)
print('\n')
print("***RECKLESS STDERR***")
for l in r.stderr.splitlines():
for l in stderr:
print(l)
print('\n')
return r
return RecklessResult(r, r.returncode, stdout, stderr)


def get_reckless_node(node_factory):
Expand All @@ -119,28 +161,13 @@ def get_reckless_node(node_factory):
return node


def check_stderr(stderr):
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 stderr.splitlines():
if len(e) < 1:
continue
# Don't err on verbosity from pip, npm
assert output_okay(e)


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 "positional arguments:" in r.stdout.splitlines()
assert "options:" in r.stdout.splitlines() or "optional arguments:" in r.stdout.splitlines()
assert r.search_stdout("positional arguments:")
assert r.search_stdout("options:") or r.search_stdout("optional arguments:")


def test_contextual_help(node_factory):
Expand All @@ -149,7 +176,7 @@ def test_contextual_help(node_factory):
'enable', 'disable', 'source']:
r = reckless([subcmd, "-h"], dir=n.lightning_dir)
assert r.returncode == 0
assert "positional arguments:" in r.stdout.splitlines()
assert r.search_stdout("positional arguments:")


def test_sources(node_factory):
Expand Down Expand Up @@ -194,18 +221,18 @@ def test_search(node_factory):
n = get_reckless_node(node_factory)
r = reckless([f"--network={NETWORK}", "search", "testplugpass"], dir=n.lightning_dir)
assert r.returncode == 0
assert 'found testplugpass in source: https://github.com/lightningd/plugins' in r.stdout
assert r.search_stdout('found testplugpass in source: https://github.com/lightningd/plugins')


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 'dependencies installed successfully' in r.stdout
assert 'plugin installed:' in r.stdout
assert 'testplugpass enabled' in r.stdout
check_stderr(r.stderr)
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)
Expand All @@ -217,10 +244,10 @@ def test_poetry_install(node_factory):
n = get_reckless_node(node_factory)
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpyproj"], dir=n.lightning_dir)
assert r.returncode == 0
assert 'dependencies installed successfully' in r.stdout
assert 'plugin installed:' in r.stdout
assert 'testplugpyproj enabled' in r.stdout
check_stderr(r.stderr)
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)
Expand All @@ -240,7 +267,7 @@ def test_local_dir_install(node_factory):
assert r.returncode == 0
r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpass"], dir=n.lightning_dir)
assert r.returncode == 0
assert 'testplugpass enabled' in r.stdout
assert r.search_stdout('testplugpass enabled')
plugin_path = Path(n.lightning_dir) / 'reckless/testplugpass'
print(plugin_path)
assert os.path.exists(plugin_path)
Expand All @@ -249,9 +276,9 @@ def test_local_dir_install(node_factory):
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 'plugin source removed' in r.stdout
assert r.search_stdout('plugin source removed')
r = reckless(['install', '-v', source_dir], dir=n.lightning_dir)
assert 'testplugpass enabled' in r.stdout
assert r.search_stdout('testplugpass enabled')
assert os.path.exists(plugin_path)


Expand All @@ -263,10 +290,10 @@ def test_disable_enable(node_factory):
r = reckless([f"--network={NETWORK}", "-v", "install", "testPlugPass"],
dir=n.lightning_dir)
assert r.returncode == 0
assert 'dependencies installed successfully' in r.stdout
assert 'plugin installed:' in r.stdout
assert 'testplugpass enabled' in r.stdout
check_stderr(r.stderr)
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)
Expand All @@ -278,7 +305,7 @@ def test_disable_enable(node_factory):
r = reckless([f"--network={NETWORK}", "-v", "enable", "testplugpass.py"],
dir=n.lightning_dir)
assert r.returncode == 0
assert 'testplugpass enabled' in r.stdout
assert r.search_stdout('testplugpass enabled')
test_plugin = {'name': str(plugin_path / 'testplugpass.py'),
'active': True, 'dynamic': True}
time.sleep(1)
Expand Down Expand Up @@ -322,3 +349,19 @@ def test_tag_install(node_factory):
if header == 'requested commit':
assert line == 'v1'
header = line


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()
Loading
Loading