pytest: move test_coinmoves.py::test_generate_coinmoves and test_plugin.py::test_spam_commands to benchmark.py
What changed, and why it matters
This commit simply moves two existing performance tests from their original test files into a dedicated benchmark file. It does not change any production code, fix bugs, or alter security behavior. The tests themselves are unchanged in substance; only their location and some benchmark plumbing are adjusted.
No security action needed. This is a test-suite reorganization. Reviewers may optionally verify that the moved tests still run correctly in CI.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit relocates test_generate_coinmoves from tests/test_coinmoves.py and test_spam_commands from tests/test_plugin.py into tests/benchmark.py. It also refactors get_bench_node to accept extra_options and updates imports. No lightningd, plugin, or library code is modified. The tests remain performance/benchmarking tests that exercise RPC throughput and database parsing under load.
Changed components
tests/benchmark.pytests/test_coinmoves.pytests/test_plugin.pyInspect captured patch +97 / −109
diff --git a/tests/benchmark.py b/tests/benchmark.py
index 3aa9db83..81ab387d 100644
--- a/tests/benchmark.py
+++ b/tests/benchmark.py
@@ -1,21 +1,27 @@
from concurrent import futures
from fixtures import * # noqa: F401,F403
-from time import time
from tqdm import tqdm
+from utils import (wait_for, TIMEOUT)
+import os
import pytest
import random
+import statistics
+import threading
+import time
num_workers = 480
num_payments = 10000
-def get_bench_node(node_factory):
+def get_bench_node(node_factory, extra_options={}):
"""Get a node which is optimized for benchmarking"""
+ options = extra_options.copy()
# The normal log-level trace makes for a lot of IO.
- node = node_factory.get_node(start=False, options={'log-level': 'info'})
+ options['log-level'] = 'info'
+ node = node_factory.get_node(start=False, options=options)
# Memleak detection here creates significant overhead!
del node.daemon.env["LIGHTNINGD_DEV_MEMLEAK"]
# Don't bother recording all our io.
@@ -125,3 +131,91 @@ def test_pay(node_factory, benchmark):
def test_start(node_factory, benchmark):
benchmark(node_factory.get_node)
+
+
+def test_generate_coinmoves(node_factory, bitcoind, executor, benchmark):
+ l1, l2, l3 = get_bench_line_graph(node_factory, 3, wait_for_announce=True)
+
+ # Route some payments
+ l1.rpc.xpay(l3.rpc.invoice(1, "test_generate_coinmoves", "test_generate_coinmoves")['bolt11'])
+ # Make some payments
+ l2.rpc.xpay(l3.rpc.invoice(1, "test_generate_coinmoves3", "test_generate_coinmoves3")['bolt11'])
+ # Receive some payments
+ l1.rpc.xpay(l2.rpc.invoice(1, "test_generate_coinmoves", "test_generate_coinmoves")['bolt11'])
+ wait_for(lambda: all([c['htlcs'] == [] for c in l1.rpc.listpeerchannels()['channels']]))
+
+ l2.stop()
+ entries = l2.db.query('SELECT * FROM channel_moves ORDER BY id;')
+ assert len(entries) == 4
+ next_id = entries[-1]['id'] + 1
+ next_timestamp = entries[-1]['timestamp'] + 1
+
+ batch = []
+ # Let's make 5 million entries.
+ for _ in range(5_000_000 // len(entries)):
+ # Random payment_hash
+ entries[0]['payment_hash'] = entries[1]['payment_hash'] = random.randbytes(32)
+ entries[2]['payment_hash'] = random.randbytes(32)
+ entries[3]['payment_hash'] = random.randbytes(32)
+ # Incrementing timestamps
+ for e in entries:
+ e['timestamp'] = next_timestamp
+ next_timestamp += 1
+
+ for e in entries:
+ batch.append((
+ next_id,
+ e['account_channel_id'],
+ e['account_nonchannel_id'],
+ e['tag_bitmap'],
+ e['credit_or_debit'],
+ e['timestamp'],
+ e['payment_hash'],
+ e['payment_part_id'],
+ e['payment_group_id'],
+ e['fees'],
+ ))
+ next_id += 1
+
+ l2.db.executemany("INSERT INTO channel_moves"
+ " (id, account_channel_id, account_nonchannel_id, tag_bitmap, credit_or_debit,"
+ " timestamp, payment_hash, payment_part_id, payment_group_id, fees)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ batch)
+ l2.start()
+
+ def measure_latency(node, stop_event):
+ latencies = []
+
+ while not stop_event.is_set():
+ time.sleep(0.1)
+
+ start = time.time()
+ node.rpc.help()
+ end = time.time()
+
+ latencies.append(end - start)
+
+ return latencies
+
+ stopme = threading.Event()
+ fut = executor.submit(measure_latency, l2, stopme)
+
+ # This makes bkpr parse it all.
+ benchmark(l2.rpc.bkpr_listbalances)
+
+ stopme.set()
+ latencies = fut.result(TIMEOUT)
+
+ # FIXME: Print this somewhere!
+ benchmark.extra_info = {"title": "Latency details:",
+ "min": min(latencies),
+ "median": statistics.median(latencies),
+ "max": max(latencies)}
+
+
+def test_spam_commands(node_factory, bitcoind, benchmark):
+ plugin = os.path.join(os.getcwd(), "tests/plugins/test_libplugin")
+ l1 = get_bench_node(node_factory, extra_options={"plugin": plugin})
+
+ benchmark(l1.rpc.spamcommand, 1_000_000)
diff --git a/tests/test_coinmoves.py b/tests/test_coinmoves.py
index 9fb31717..291f857b 100644
--- a/tests/test_coinmoves.py
+++ b/tests/test_coinmoves.py
@@ -7,10 +7,7 @@ from utils import (
import os
import unittest
import pytest
-import random
import re
-import threading
-import statistics
import time
from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND
@@ -2091,86 +2088,3 @@ def test_migration_no_bkpr(node_factory, bitcoind):
check_channel_moves(l2, expected_channel2)
check_chain_moves(l1, expected_chain1)
check_chain_moves(l2, expected_chain2)
-
-
-def test_generate_coinmoves(node_factory, bitcoind, executor):
- l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True, opts={'log-level': 'info'})
-
- # Route some payments
- l1.rpc.xpay(l3.rpc.invoice(1, "test_generate_coinmoves", "test_generate_coinmoves")['bolt11'])
- # Make some payments
- l2.rpc.xpay(l3.rpc.invoice(1, "test_generate_coinmoves3", "test_generate_coinmoves3")['bolt11'])
- # Receive some payments
- l1.rpc.xpay(l2.rpc.invoice(1, "test_generate_coinmoves", "test_generate_coinmoves")['bolt11'])
- wait_for(lambda: all([c['htlcs'] == [] for c in l1.rpc.listpeerchannels()['channels']]))
-
- l2.stop()
- entries = l2.db.query('SELECT * FROM channel_moves ORDER BY id;')
- assert len(entries) == 4
- next_id = entries[-1]['id'] + 1
- next_timestamp = entries[-1]['timestamp'] + 1
-
- batch = []
- # Let's make 5 million entries.
- for _ in range(5_000_000 // len(entries)):
- # Random payment_hash
- entries[0]['payment_hash'] = entries[1]['payment_hash'] = random.randbytes(32)
- entries[2]['payment_hash'] = random.randbytes(32)
- entries[3]['payment_hash'] = random.randbytes(32)
- # Incrementing timestamps
- for e in entries:
- e['timestamp'] = next_timestamp
- next_timestamp += 1
-
- for e in entries:
- batch.append((
- next_id,
- e['account_channel_id'],
- e['account_nonchannel_id'],
- e['tag_bitmap'],
- e['credit_or_debit'],
- e['timestamp'],
- e['payment_hash'],
- e['payment_part_id'],
- e['payment_group_id'],
- e['fees'],
- ))
- next_id += 1
-
- l2.db.executemany("INSERT INTO channel_moves"
- " (id, account_channel_id, account_nonchannel_id, tag_bitmap, credit_or_debit,"
- " timestamp, payment_hash, payment_part_id, payment_group_id, fees)"
- " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
- batch)
-
- # Memleak detection here creates significant overhead!
- del l2.daemon.env["LIGHTNINGD_DEV_MEMLEAK"]
- # Don't bother recording all our io.
- del l2.daemon.opts['dev-save-plugin-io']
- l2.start()
-
- def measure_latency(node, stop_event):
- latencies = []
-
- while not stop_event.is_set():
- time.sleep(0.1)
-
- start = time.time()
- node.rpc.help()
- end = time.time()
-
- latencies.append(end - start)
-
- return latencies
-
- stopme = threading.Event()
- fut = executor.submit(measure_latency, l2, stopme)
-
- # This makes bkpr parse it all.
- l2.rpc.bkpr_listbalances()
-
- stopme.set()
- # Latency under 1 second
- latencies = fut.result(TIMEOUT)
- print(f"RESULT: min, median, max: {min(latencies)}, {statistics.median(latencies)}, {max(latencies)}")
- assert max(latencies) < 1
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index b7c2242c..a036ecc2 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -4719,23 +4719,3 @@ def test_openchannel_hook_channel_type(node_factory, bitcoind):
l2.daemon.wait_for_log(r"plugin-openchannel_hook_accepter.py: accept by design: channel_type {'bits': \[12, 22\], 'names': \['static_remotekey/even', 'anchors/even'\]}")
else:
l2.daemon.wait_for_log(r"plugin-openchannel_hook_accepter.py: accept by design: channel_type {'bits': \[12\], 'names': \['static_remotekey/even'\]}")
-
-
-@pytest.mark.slow_test
-def test_spam_commands(node_factory, bitcoind):
- plugin = os.path.join(os.getcwd(), "tests/plugins/test_libplugin")
- l1 = node_factory.get_node(options={"plugin": plugin, 'log-level': 'info'},
- start=False)
-
- # Memleak detection here creates significant overhead!
- del l1.daemon.env["LIGHTNINGD_DEV_MEMLEAK"]
- # Don't bother recording all our io.
- del l1.daemon.opts['dev-save-plugin-io']
- l1.start()
-
- start_time = time.time()
- l1.rpc.spamcommand(1_000_000)
- duration = time.time() - start_time
-
- # Change 100 to 0 to get test to fail so you can see the result!
- assert duration < 100
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.