tests: properly clean-up MockLNWallets after tests finish
What changed, and why it matters
This commit only changes the project's internal unit tests. It makes test-only mock Lightning wallets get created and cleaned up in a more centralized way so temporary files and background tasks are reliably removed after each test. There is no change to the actual Electrum wallet or Lightning code that end users run, so this does not affect real-world security.
No security action needed. Treat as routine test maintenance.
Security signals we found
No production code modified
Changes limited to tests/ directory
Refactors test resource cleanup only
No cryptographic, network, or wallet logic changes
Evidence from the diff
The patch refactors test infrastructure: ElectrumTestCase now owns creation of MockLNWallet instances via create_mock_lnwallet() and stops/clears them in asyncTearDown(). test_lnpeer’s create_mock_lnwallet is renamed to _create_mock_lnwallet and accepts an external data_dir. Test cases in test_lnchannel.py, test_lnpeer.py, and test_onion_message.py are updated to use the shared helper. This is purely a test-hygiene/cleanup change; no production code paths are modified.
Changed components
tests/__init__.pytests/test_lnchannel.pytests/test_lnpeer.pytests/test_onion_message.pyInspect captured patch +75 / −35
diff --git a/tests/__init__.py b/tests/__init__.py
index b2b649b..fec8240 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -6,14 +6,19 @@ import tempfile
import shutil
import functools
import inspect
+from typing import TYPE_CHECKING, List
import electrum
import electrum.logging
from electrum import constants
from electrum import util
+from electrum.util import OldTaskGroup
from electrum.logging import Logger
from electrum.wallet import restore_wallet_from_text
+if TYPE_CHECKING:
+ from .test_lnpeer import MockLNWallet
+
# Set this locally to make the test suite run faster.
# If set, unit tests that would normally test functions with multiple implementations,
@@ -64,8 +69,11 @@ class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger):
# or if a prior test raised during `setUp` or `asyncSetUp` and never released the lock.
raise Exception("timed out waiting for test_lock")
super().setUp()
- self.electrum_path = tempfile.mkdtemp(prefix="electrum-unittest-base-")
+ self.unittest_base_path = tempfile.mkdtemp(prefix="electrum-unittest-base-")
+ self.electrum_path = os.path.join(self.unittest_base_path, "electrum")
+ util.make_dir(self.electrum_path)
assert util._asyncio_event_loop is None, "global event loop already set?!"
+ self._lnworkers_created = [] # type: List[MockLNWallet]
async def asyncSetUp(self):
await super().asyncSetUp()
@@ -75,13 +83,33 @@ class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger):
loop.set_debug(False)
util._asyncio_event_loop = loop
+ async def asyncTearDown(self):
+ # clean up lnworkers
+ async with OldTaskGroup() as group:
+ for lnworker in self._lnworkers_created:
+ await group.spawn(lnworker.stop())
+ self._lnworkers_created.clear()
+ await super().asyncTearDown()
+
def tearDown(self):
util.callback_mgr.clear_all_callbacks()
- shutil.rmtree(self.electrum_path)
+ shutil.rmtree(self.unittest_base_path)
super().tearDown()
util._asyncio_event_loop = None # cleared here, at the ~last possible moment. asyncTearDown is too early.
self._test_lock.release()
+ def create_mock_lnwallet(
+ self,
+ *,
+ name: str,
+ has_anchors: bool,
+ ) -> 'MockLNWallet':
+ from .test_lnpeer import _create_mock_lnwallet
+ data_dir = tempfile.mkdtemp(prefix="lnwallet-", dir=self.unittest_base_path)
+ lnwallet = _create_mock_lnwallet(name=name, has_anchors=has_anchors, data_dir=data_dir)
+ self._lnworkers_created.append(lnwallet)
+ return lnwallet
+
def as_testnet(func):
"""Function decorator to run a single unit test in testnet mode.
diff --git a/tests/test_lnchannel.py b/tests/test_lnchannel.py
index 7a81dcd..2081dd7 100644
--- a/tests/test_lnchannel.py
+++ b/tests/test_lnchannel.py
@@ -123,8 +123,8 @@ def create_channel_state(
def create_test_channels(
*,
- alice_lnwallet: 'MockLNWallet' = None,
- bob_lnwallet: 'MockLNWallet' = None,
+ alice_lnwallet: 'MockLNWallet',
+ bob_lnwallet: 'MockLNWallet',
feerate=6000,
local_msat=None,
remote_msat=None,
@@ -137,12 +137,6 @@ def create_test_channels(
if random_seed is None: # needed for deterministic randomness
random_seed = os.urandom(32)
random_gen = PRNG(random_seed)
- if alice_lnwallet is None:
- from .test_lnpeer import create_mock_lnwallet
- alice_lnwallet = create_mock_lnwallet(name="alice", has_anchors=anchor_outputs)
- if bob_lnwallet is None:
- from .test_lnpeer import create_mock_lnwallet
- bob_lnwallet = create_mock_lnwallet(name="bob", has_anchors=anchor_outputs)
alice_name = alice_lnwallet.name
bob_name = bob_lnwallet.name
alice_pubkey = alice_lnwallet.node_keypair.pubkey
@@ -267,12 +261,21 @@ class TestFee(ElectrumTestCase):
test
https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2
"""
+
+ async def asyncSetUp(self):
+ await super().asyncSetUp()
+ self.alice_lnwallet = self.create_mock_lnwallet(name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ self.bob_lnwallet = self.create_mock_lnwallet(name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS)
+
async def test_fee(self):
alice_channel, bob_channel = create_test_channels(
feerate=253,
local_msat=10_000_000_000,
remote_msat=5_000_000_000,
- anchor_outputs=self.TEST_ANCHOR_CHANNELS)
+ anchor_outputs=self.TEST_ANCHOR_CHANNELS,
+ alice_lnwallet=self.alice_lnwallet,
+ bob_lnwallet=self.bob_lnwallet,
+ )
expected_value = 9_999_056 if self.TEST_ANCHOR_CHANNELS else 9_999_817
self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
@@ -297,10 +300,14 @@ class TestChannel(ElectrumTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
+ self.alice_lnwallet = self.create_mock_lnwallet(name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ self.bob_lnwallet = self.create_mock_lnwallet(name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS)
+
# Create a test channel which will be used for the duration of this
# unittest. The channel will be funded evenly with Alice having 5 BTC,
# and Bob having 5 BTC.
- self.alice_channel, self.bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS)
+ self.alice_channel, self.bob_channel = create_test_channels(
+ anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)
self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage)
@@ -785,8 +792,14 @@ class TestChannelAnchors(TestChannel):
class TestAvailableToSpend(ElectrumTestCase):
+ async def asyncSetUp(self):
+ await super().asyncSetUp()
+ self.alice_lnwallet = self.create_mock_lnwallet(name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ self.bob_lnwallet = self.create_mock_lnwallet(name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS)
+
async def test_DesyncHTLCs(self):
- alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS)
+ alice_channel, bob_channel = create_test_channels(
+ anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)
self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499980692000, alice_channel.available_to_spend(LOCAL))
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
@@ -837,7 +850,10 @@ class TestAvailableToSpend(ElectrumTestCase):
local_msat=4000000000,
remote_msat=4000000000,
local_max_inflight=1000000000,
- remote_max_inflight=2000000000)
+ remote_max_inflight=2000000000,
+ alice_lnwallet=self.alice_lnwallet,
+ bob_lnwallet=self.bob_lnwallet,
+ )
# alice can send 20 but bob can only receive 10, because of stricter receiving rules
self.assertEqual(2000000000, alice_channel.available_to_spend(LOCAL))
@@ -893,8 +909,11 @@ class TestAvailableToSpendAnchors(TestAvailableToSpend):
class TestChanReserve(ElectrumTestCase):
- def setUp(self):
- alice_channel, bob_channel = create_test_channels(anchor_outputs=False)
+ async def asyncSetUp(self):
+ await super().asyncSetUp()
+ alice_lnwallet = self.create_mock_lnwallet(name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ bob_lnwallet = self.create_mock_lnwallet(name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ alice_channel, bob_channel = create_test_channels(anchor_outputs=False, alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)
alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000)
# We set Bob's channel reserve to a value that is larger than
# his current balance in the channel. This will ensure that
@@ -1027,9 +1046,14 @@ class TestChanReserveAnchors(TestChanReserve):
class TestDust(ElectrumTestCase):
+ async def asyncSetUp(self):
+ await super().asyncSetUp()
+ self.alice_lnwallet = self.create_mock_lnwallet(name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS)
+ self.bob_lnwallet = self.create_mock_lnwallet(name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS)
+
async def test_DustLimit(self):
"""Test that addition of an HTLC below the dust limit changes the balances."""
- alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS)
+ alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS, alice_lnwallet=self.alice_lnwallet, bob_lnwallet=self.bob_lnwallet)
dust_limit_alice = alice_channel.config[LOCAL].dust_limit_sat
dust_limit_bob = bob_channel.config[LOCAL].dust_limit_sat
self.assertLess(dust_limit_alice, dust_limit_bob)
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 0b14a6a..3192398 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -137,9 +137,8 @@ class MockStandardWallet(Standard_Wallet):
assert passphrase
return passphrase # lol, super secure name
-def create_mock_lnwallet(*, name, has_anchors) -> 'MockLNWallet':
- _user_dir = tempfile.mkdtemp(prefix="electrum-lnpeer-test-") # TODO clean-up after??
- config = SimpleConfig({}, read_user_dir_function=lambda: _user_dir)
+def _create_mock_lnwallet(*, name, has_anchors, data_dir: str) -> 'MockLNWallet':
+ config = SimpleConfig({}, read_user_dir_function=lambda: data_dir)
config.ENABLE_ANCHOR_CHANNELS = has_anchors
config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0
@@ -154,7 +153,6 @@ def create_mock_lnwallet(*, name, has_anchors) -> 'MockLNWallet':
lnworker = wallet.lnworker
assert isinstance(lnworker, MockLNWallet), f"{lnworker=!r}"
- lnworker._user_dir = _user_dir
lnworker.lnpeermgr.network = network
lnworker.logger.info(f"created LNWallet[{name}] with nodeID={lnworker.node_keypair.pubkey.hex()}")
return lnworker
@@ -410,16 +408,8 @@ class TestPeer(ElectrumTestCase):
def setUp(self):
super().setUp()
self.GRAPH_DEFINITIONS = copy.deepcopy(_GRAPH_DEFINITIONS)
- self._lnworkers_created = [] # type: List[MockLNWallet]
async def asyncTearDown(self):
- # clean up lnworkers
- async with OldTaskGroup() as group:
- for lnworker in self._lnworkers_created:
- await group.spawn(lnworker.stop())
- for lnworker in self._lnworkers_created:
- shutil.rmtree(lnworker._user_dir)
- self._lnworkers_created.clear()
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}
await super().asyncTearDown()
@@ -501,8 +491,7 @@ class TestPeer(ElectrumTestCase):
def prepare_lnwallets(self, graph_definition) -> Mapping[str, MockLNWallet]:
workers = {} # type: Dict[str, MockLNWallet]
for a, definition in graph_definition.items():
- workers[a] = create_mock_lnwallet(name=a, has_anchors=self.TEST_ANCHOR_CHANNELS)
- self._lnworkers_created.extend(list(workers.values()))
+ workers[a] = self.create_mock_lnwallet(name=a, has_anchors=self.TEST_ANCHOR_CHANNELS)
return workers
def prepare_chans_and_peers_in_graph(
diff --git a/tests/test_onion_message.py b/tests/test_onion_message.py
index 058e6b8..5f35b35 100644
--- a/tests/test_onion_message.py
+++ b/tests/test_onion_message.py
@@ -25,7 +25,6 @@ from electrum.util import bfh, read_json_file, OldTaskGroup, get_asyncio_loop
from electrum.logging import console_stderr_handler
from . import ElectrumTestCase
-from .test_lnpeer import create_mock_lnwallet
TIME_STEP = 0.01 # run tests 100 x faster
@@ -353,7 +352,7 @@ class TestOnionMessageManager(ElectrumTestCase):
async def test_request_and_reply(self):
n = MockNetwork()
- lnw = create_mock_lnwallet(name='test_request_and_reply', has_anchors=False)
+ lnw = self.create_mock_lnwallet(name='test_request_and_reply', has_anchors=False)
def slow(*args, **kwargs):
time.sleep(2*TIME_STEP)
@@ -399,7 +398,7 @@ class TestOnionMessageManager(ElectrumTestCase):
async def test_forward(self):
n = MockNetwork()
- lnw = create_mock_lnwallet(name='alice', has_anchors=False)
+ lnw = self.create_mock_lnwallet(name='alice', has_anchors=False)
lnw.node_keypair = self.alice
self.was_sent = False
@@ -436,7 +435,7 @@ class TestOnionMessageManager(ElectrumTestCase):
async def test_receive_unsolicited(self):
n = MockNetwork()
- lnw = create_mock_lnwallet(name='dave', has_anchors=False)
+ lnw = self.create_mock_lnwallet(name='dave', has_anchors=False)
lnw.node_keypair = self.dave
t = OnionMessageManager(lnw)
Why this scored 14/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.