Skip to content

Commit e43a4a9

Browse files
committed
pyln-testing: check plugin notifications against any extant notification schemas.
Note that we need a workaround for deprecated APIs where "channel_state_changed" output "null" which violated the schema. Signed-off-by: Rusty Russell <[email protected]>
1 parent 9e7be80 commit e43a4a9

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

contrib/pyln-testing/pyln/testing/fixtures.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pyln.testing.utils import NodeFactory, BitcoinD, ElementsD, env, LightningNode, TEST_DEBUG, TEST_NETWORK
44
from pyln.client import Millisatoshi
55
from typing import Dict
6+
from pathlib import Path
67

78
import json
89
import jsonschema # type: ignore
@@ -496,6 +497,7 @@ def map_node_error(nodes, f, msg):
496497
map_node_error(nf.nodes, checkBroken, "had BROKEN messages")
497498
map_node_error(nf.nodes, lambda n: not n.allow_warning and n.daemon.is_in_log(r' WARNING:'), "had warning messages")
498499
map_node_error(nf.nodes, checkReconnect, "had unexpected reconnections")
500+
map_node_error(nf.nodes, checkPluginJSON, "had malformed hooks/notifications")
499501

500502
# On any bad gossip complaints, print out every nodes' gossip_store
501503
if map_node_error(nf.nodes, checkBadGossip, "had bad gossip messages"):
@@ -617,6 +619,57 @@ def checkBroken(node):
617619
return 0
618620

619621

622+
def checkPluginJSON(node):
623+
if node.bad_notifications:
624+
return 0
625+
626+
try:
627+
notificationfiles = os.listdir('doc/schemas/notification')
628+
except FileNotFoundError:
629+
notificationfiles = []
630+
631+
notifications = {}
632+
for fname in notificationfiles:
633+
if fname.endswith('.json'):
634+
base = fname.replace('.json', '')
635+
# Request is 0 and Response is 1
636+
notifications[base] = _load_schema(os.path.join('doc/schemas/notification', fname))
637+
638+
# FIXME: add doc/schemas/hook/
639+
hooks = {}
640+
641+
for f in (Path(node.daemon.lightning_dir) / "plugin-io").iterdir():
642+
# e.g. hook_in-peer_connected-124567-358
643+
io = json.loads(f.read_text())
644+
parts = f.name.split('-')
645+
if parts[0] == 'hook_in':
646+
schema = hooks.get(parts[1])
647+
req = io['result']
648+
direction = 1
649+
elif parts[0] == 'hook_out':
650+
schema = hooks.get(parts[1])
651+
req = io['params']
652+
direction = 0
653+
else:
654+
assert parts[0] == 'notification_out'
655+
schema = notifications.get(parts[1])
656+
# The notification is wrapped in an object of its own name.
657+
req = io['params'][parts[1]]
658+
direction = 1
659+
660+
# Until v26.09, with channel_state_changed.null_scid, that notification will be non-schema compliant.
661+
if f.name.startswith('notification_out-channel_state_changed-') and node.daemon.opts.get('allow-deprecated-apis', True) is True:
662+
continue
663+
664+
if schema is not None:
665+
try:
666+
schema[direction].validate(req)
667+
except jsonschema.exceptions.ValidationError as e:
668+
print(f"Failed validating {f}: {e}")
669+
return 1
670+
return 0
671+
672+
620673
def checkBadReestablish(node):
621674
if node.daemon.is_in_log('Bad reestablish'):
622675
return 1

contrib/pyln-testing/pyln/testing/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,11 +789,13 @@ def __init__(self, node_id, lightning_dir, bitcoind, executor, valgrind, may_fai
789789
jsonschemas={},
790790
valgrind_plugins=True,
791791
executable=None,
792+
bad_notifications=False,
792793
**kwargs):
793794
self.bitcoin = bitcoind
794795
self.executor = executor
795796
self.may_fail = may_fail
796797
self.may_reconnect = may_reconnect
798+
self.bad_notifications = bad_notifications
797799
self.broken_log = broken_log
798800
self.allow_bad_gossip = allow_bad_gossip
799801
self.allow_warning = allow_warning
@@ -853,6 +855,10 @@ def __init__(self, node_id, lightning_dir, bitcoind, executor, valgrind, may_fai
853855
if self.cln_version >= "v24.11":
854856
self.daemon.opts.update({"autoconnect-seeker-peers": 0})
855857

858+
jsondir = Path(lightning_dir) / "plugin-io"
859+
jsondir.mkdir()
860+
self.daemon.opts['dev-save-plugin-io'] = jsondir
861+
856862
if options is not None:
857863
self.daemon.opts.update(options)
858864
dsn = db.get_dsn()
@@ -1601,6 +1607,7 @@ def split_options(self, opts):
16011607
'broken_log',
16021608
'allow_warning',
16031609
'may_reconnect',
1610+
'bad_notifications',
16041611
'random_hsm',
16051612
'feerates',
16061613
'wait_for_bitcoind_sync',

tests/test_misc.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2723,6 +2723,9 @@ def test_new_node_is_mainnet(node_factory):
27232723
assert not os.path.isfile(os.path.join(netdir, "lightningd-bitcoin.pid"))
27242724
assert os.path.isfile(os.path.join(basedir, "lightningd-bitcoin.pid"))
27252725

2726+
# Teardown expects this to exist...
2727+
os.mkdir(basedir + "/plugin-io")
2728+
27262729

27272730
def test_unicode_rpc(node_factory, executor, bitcoind):
27282731
node = node_factory.get_node()
@@ -3659,7 +3662,8 @@ def test_version_reexec(node_factory, bitcoind):
36593662
# We use a file to tell our openingd wrapper where the real one is
36603663
with open(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "openingd-real"), 'w') as f:
36613664
f.write(os.path.abspath('lightningd/lightning_openingd'))
3662-
3665+
# Internal restart doesn't work well with --dev-save-plugin-io
3666+
del l1.daemon.opts['dev-save-plugin-io']
36633667
l1.start()
36643668
# This is a "version" message
36653669
verfile = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "openingd-version")
@@ -4534,7 +4538,11 @@ def test_setconfig_changed(node_factory, bitcoind):
45344538

45354539
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
45364540
def test_recover_command(node_factory, bitcoind):
4537-
l1, l2 = node_factory.get_nodes(2)
4541+
l1 = node_factory.get_node(start=False)
4542+
# Internal restart doesn't work well with --dev-save-plugin-io
4543+
del l1.daemon.opts['dev-save-plugin-io']
4544+
l1.start()
4545+
l2 = node_factory.get_node()
45384546

45394547
l1oldid = l1.info['id']
45404548

0 commit comments

Comments
 (0)