Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ jobs:
tests/test_misc.py::test_ipv4_and_ipv6
tests/test_connection.py::test_websocket
tests/test_connection.py::test_wss_proxy
tests/test_plugin.py::test_inline_plugin_wait_for_log_no_selfmatch
run: |
VALGRIND=0 uv run pytest $PYTEST_TESTS -n $(sysctl -n hw.ncpu) ${PYTEST_OPTS}
46 changes: 45 additions & 1 deletion contrib/pyln-testing/pyln/testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pyln.client import Millisatoshi
from pyln.client import NodeVersion
from pyln.client import Plugin
from pyln.client.plugin import PluginLogHandler

import ephemeral_port_reserve # type: ignore
import tempfile
Expand Down Expand Up @@ -2050,6 +2051,26 @@ def killall(self, expected_successes):
return not unexpected_fail, err_msgs


class _NoPylnInternalsFilter(logging.Filter):
"""Drop log records generated inside the pyln packages themselves.

An inline plugin's Plugin() lives in the test process, so its
PluginLogHandler on the root logger would forward pyln's own machinery
logs into the node's log. wait_for_logs()'s 'Waiting for [pattern]'
announcement embeds the pattern verbatim, lands in the very log being
scanned, and matches itself, silently reducing the wait to a no-op.
Only records from outside pyln (i.e. the plugin author's own logging)
may be forwarded.
"""
PYLN_DIRS = tuple(
os.path.dirname(os.path.abspath(f)) + os.sep
for f in (__file__,
sys.modules[PluginLogHandler.__module__].__file__))

def filter(self, record):
return not os.path.abspath(record.pathname).startswith(self.PYLN_DIRS)


def _inline_plugin(node, setup_fn):
"""Set up an inline plugin serve thread for a not-yet-started node.

Expand All @@ -2069,10 +2090,33 @@ def greet(name, plugin):
"""
sock_path = os.path.join(node.daemon.lightning_dir, TEST_NETWORK, 'inline-plugin.sock')
srv = socket.socket(socket.AF_UNIX)
srv.bind(sock_path)
try:
srv.bind(sock_path)
except OSError as e:
# AF_UNIX caps the bind path (108 bytes on Linux, 104 on macOS),
# and the node dir embeds the (possibly long) test name. Bind
# through a short symlink alias to the socket's directory -- the
# bind-side analogue of UnixSocket.connect's Darwin workaround
# (bind can't go through a dangling final-component symlink, so
# alias the directory rather than the socket). The socket file
# still lands at sock_path, where the shim's cwd-relative connect
# expects it.
if e.args[0] != "AF_UNIX path too long":
raise
alias_dir = tempfile.mkdtemp(prefix='pyln-sock-')
alias = os.path.join(alias_dir, 'd')
os.symlink(os.path.dirname(sock_path), alias)
try:
srv.bind(os.path.join(alias, os.path.basename(sock_path)))
finally:
os.unlink(alias)
os.rmdir(alias_dir)
srv.listen(1)

plugin = Plugin(autopatch=False)
for handler in logging.getLogger().handlers:
if isinstance(handler, PluginLogHandler) and handler.plugin is plugin:
handler.addFilter(_NoPylnInternalsFilter())
setup_fn(plugin)

def serve():
Expand Down
33 changes: 33 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import ast
import copy
import json
import logging
import os
import pytest
import random
Expand Down Expand Up @@ -1803,6 +1804,38 @@ def record_lookup(plugin):
assert len(results['sendpay_failure']) == 1


def test_inline_plugin_wait_for_log_no_selfmatch(node_factory):
"""On inline-plugin nodes the test process's logging is forwarded into
the node's log. wait_for_log()'s own 'Waiting for [pattern]'
announcement embeds the pattern, so with test logging at DEBUG it used
to land in the scanned log and match itself, reducing the wait to a
no-op (#9343). A pattern that never appears must genuinely time out.
"""
def setup(plugin):
@plugin.method('inline_ping')
def inline_ping(plugin):
logging.info("AUTHOR_LOG_MARKER_9343")
return {'pong': True}

l1 = node_factory.get_node(inline_plugin=setup)

root = logging.getLogger()
old_level = root.level
root.setLevel(logging.DEBUG)
try:
# The plugin author's own logging must still be forwarded...
assert l1.rpc.call('inline_ping') == {'pong': True}
l1.daemon.wait_for_log('AUTHOR_LOG_MARKER_9343')
# ...but pyln's internal announcements must not be, so a pattern
# that never appears genuinely times out instead of matching the
# forwarded 'Waiting for [pattern]' line.
with pytest.raises(TimeoutError):
l1.daemon.wait_for_log('SELFMATCH_SENTINEL_NEVER_LOGGED',
timeout=5)
finally:
root.setLevel(old_level)


def test_rpc_command_hook(node_factory):
"""Test the `rpc_command` hook chain"""
plugin = [
Expand Down
Loading